diff --git a/.gitignore b/.gitignore index b2021982..aeac09df 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ flutter_*.png *.DS_Store *.iml .flutter-plugins* -coverage/ \ No newline at end of file +coverage/ + +*.g.dart \ No newline at end of file diff --git a/lib/core/models/chat_model.dart b/lib/core/models/chat_model.dart index 92d9a4dc..3586e16d 100644 --- a/lib/core/models/chat_model.dart +++ b/lib/core/models/chat_model.dart @@ -18,40 +18,46 @@ class ChatModel { @HiveField(1) final List userIds; @HiveField(2) - final String? photo; + final List? admins; @HiveField(3) - final ChatType type; + final List? creators; @HiveField(4) - Uint8List? photoBytes; + final String? photo; @HiveField(5) - String? id; + final ChatType type; @HiveField(6) - final List? admins; + Uint8List? photoBytes; @HiveField(7) - final String? description; + String? id; @HiveField(8) - final DateTime? lastMessageTimestamp; + final String? description; @HiveField(9) - final bool isArchived; + final DateTime? lastMessageTimestamp; @HiveField(10) - final bool isMuted; + final bool isArchived; @HiveField(11) - final String? draft; + final bool isMuted; @HiveField(12) - final bool isMentioned; + final String? draft; @HiveField(13) - List messages; + final bool isMentioned; @HiveField(14) + List messages; + @HiveField(15) final DateTime? muteUntil; // Add this field + @HiveField(16) + final bool messagingPermission; //1->anyone 0->admins + ChatModel({ required this.title, required this.userIds, + this.admins, + this.creators, this.photo, required this.type, this.id, this.photoBytes, - this.admins, this.description, this.lastMessageTimestamp, this.isArchived = false, @@ -60,6 +66,7 @@ class ChatModel { this.draft, required this.messages, this.muteUntil, // Initialize this field + this.messagingPermission = true, }); Future _setPhotoBytes() async { @@ -92,6 +99,7 @@ class ChatModel { other.photo == photo && other.id == id && other.admins == admins && + other.creators == creators && other.description == description && other.lastMessageTimestamp == lastMessageTimestamp && other.isArchived == isArchived && @@ -99,7 +107,8 @@ class ChatModel { other.muteUntil == muteUntil && other.draft == draft && other.isMentioned == isMentioned && - other.messages == messages; + other.messages == messages&& + other.messagingPermission == messagingPermission; } @override @@ -115,8 +124,10 @@ class ChatModel { isArchived.hashCode ^ isMuted.hashCode ^ draft.hashCode ^ + creators.hashCode ^ isMentioned.hashCode ^ - messages.hashCode; // Include messages in hashCode + messages.hashCode^ // Include messages in hashCode + messagingPermission.hashCode; } @override @@ -128,6 +139,7 @@ class ChatModel { 'photo: $photo,\n' 'id: $id,\n' 'admins: $admins,\n' + 'creators: $creators,\n' 'description: $description,\n' 'lastMessageTimestamp: $lastMessageTimestamp,\n' 'isArchived: $isArchived,\n' @@ -135,6 +147,7 @@ class ChatModel { 'draft: $draft,\n' 'isMentioned: $isMentioned,\n' 'messages: $messages,\n' // Add messages to the string representation + 'messagingPermission: $messagingPermission,\n' ')'); } @@ -146,6 +159,7 @@ class ChatModel { String? id, Uint8List? photoBytes, List? admins, + List? creators, String? description, DateTime? lastMessageTimestamp, bool? isArchived, @@ -154,6 +168,7 @@ class ChatModel { String? draft, bool? isMentioned, List? messages, + bool? messagingPermission, }) { return ChatModel( title: title ?? this.title, @@ -163,6 +178,7 @@ class ChatModel { id: id ?? this.id, photoBytes: photoBytes ?? this.photoBytes, admins: admins ?? this.admins, + creators: creators ?? this.creators, description: description ?? this.description, lastMessageTimestamp: lastMessageTimestamp ?? this.lastMessageTimestamp, isArchived: isArchived ?? this.isArchived, @@ -171,6 +187,7 @@ class ChatModel { draft: draft ?? this.draft, isMentioned: isMentioned ?? this.isMentioned, messages: messages ?? this.messages, + messagingPermission: messagingPermission ?? this.messagingPermission, ); } @@ -182,6 +199,7 @@ class ChatModel { 'photo': photo, 'id': id, 'admins': admins, + 'creators': creators, 'description': description, 'lastMessageTimestamp': lastMessageTimestamp?.toIso8601String(), 'isArchived': isArchived, @@ -189,6 +207,7 @@ class ChatModel { 'muteUntil': muteUntil?.toIso8601String(), // Add this field 'draft': draft, 'isMentioned': isMentioned, + 'messagingPermission': messagingPermission, 'messages': messages.map((message) => message.toMap()).toList(), }; } diff --git a/lib/core/models/message_model.dart b/lib/core/models/message_model.dart index a1874beb..04931c39 100644 --- a/lib/core/models/message_model.dart +++ b/lib/core/models/message_model.dart @@ -6,10 +6,6 @@ import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/chat/classes/message_content.dart'; -import 'package:telware_cross_platform/features/stories/utils/utils_functions.dart'; - -import '../constants/server_constants.dart'; - part 'message_model.g.dart'; @HiveType(typeId: 6) @@ -29,55 +25,40 @@ class MessageModel { @HiveField(6) String? id; @HiveField(7) - final String? photo; + final Map userStates; // userID -> state of the message @HiveField(8) - Uint8List? photoBytes; + final bool isPinned; @HiveField(9) - final Map userStates; // userID -> state of the message + final String? parentMessage; @HiveField(10) - final bool isPinned; + final String localId; @HiveField(11) - final String? parentMessage; + final bool isForward; @HiveField(12) - final String localId; + final bool isAnnouncement; @HiveField(13) - final bool? isForward; + List threadMessages; + @HiveField(14) + final bool isEdited; // - MessageModel( - {this.autoDeleteTimestamp, - required this.senderId, - required this.messageType, - required this.messageContentType, - this.content, - required this.timestamp, - this.id, - this.photo, - this.photoBytes, - required this.userStates, - this.isPinned = false, - this.parentMessage, - this.localId = '', - this.isForward = false}); - - Future _setPhotoBytes() async { - if (photo == null || photo!.isEmpty) return; - - String url = - photo!.startsWith('http') ? photo! : '$API_URL_PICTURES/$photo'; - - if (url.isEmpty) return; - - Uint8List? tempImage; - try { - tempImage = await downloadImage(url); - if (tempImage != null) { - photoBytes = tempImage; - } - } catch (e) { - photoBytes = null; - } - } + MessageModel({ + this.autoDeleteTimestamp, + required this.senderId, + required this.messageType, + required this.messageContentType, + this.content, + required this.timestamp, + this.id, + required this.userStates, + this.parentMessage, + this.localId = '', + this.isPinned = false, + this.isEdited = false, + this.isForward = false, + this.isAnnouncement = false, + List? threadMessages, + }) : threadMessages = threadMessages ?? []; void updateUserState(String userId, MessageState state) { userStates[userId] = state; @@ -93,15 +74,17 @@ class MessageModel { return other.messageType == messageType && other.messageContentType == messageContentType && + other.parentMessage == parentMessage && other.senderId == senderId && other.content == content && other.timestamp == timestamp && other.autoDeleteTimestamp == autoDeleteTimestamp && other.id == id && - other.photo == photo && - other.photoBytes == photoBytes && + other.threadMessages == threadMessages && + other.isAnnouncement == isAnnouncement && other.localId == localId && other.isForward == isForward && + other.isEdited == isEdited && other.userStates == userStates; } @@ -114,8 +97,10 @@ class MessageModel { content.hashCode ^ timestamp.hashCode ^ id.hashCode ^ - photo.hashCode ^ - photoBytes.hashCode ^ + isAnnouncement.hashCode ^ + parentMessage.hashCode ^ + isEdited.hashCode ^ + threadMessages.hashCode ^ isForward.hashCode ^ localId.hashCode ^ userStates.hashCode; @@ -129,13 +114,16 @@ class MessageModel { 'timestamp: $timestamp,\n' 'autoDeleteTimestamp: $autoDeleteTimestamp,\n' 'id: $id,\n' - 'photo: $photo,\n' 'userStates: $userStates,\n' 'messageType: ${messageType.name},\n' 'messageContentType: ${messageContentType.name},\n' - 'isPhotoBytesSet: ${photoBytes != null}\n' 'localId: $localId\n' + 'isAnnouncement: $isAnnouncement\n' 'isForward: $isForward\n' + 'isPinned: $isPinned\n' + 'isEdited: $isEdited\n' + 'threadMessages: $threadMessages' + 'parentMessage: $parentMessage' ')'); } @@ -153,21 +141,28 @@ class MessageModel { String? localId, bool? isForward, bool? isPinned, + bool? isAnnouncement, + bool? isEdited, + String? parentMessage, + List? threadMessages, }) { return MessageModel( - senderId: senderId ?? this.senderId, - content: content ?? this.content, - timestamp: timestamp ?? this.timestamp, - autoDeleteTimestamp: autoDeleteTimestamp ?? this.autoDeleteTimestamp, - id: id ?? this.id, - photo: photo ?? this.photo, - photoBytes: photoBytes ?? this.photoBytes, - userStates: userStates ?? Map.from(this.userStates), - messageType: messageType ?? this.messageType, - messageContentType: messageContentType ?? this.messageContentType, - localId: localId ?? this.localId, - isForward: isForward ?? this.isForward, - isPinned: isPinned ?? this.isPinned); + senderId: senderId ?? this.senderId, + content: content ?? this.content, + timestamp: timestamp ?? this.timestamp, + autoDeleteTimestamp: autoDeleteTimestamp ?? this.autoDeleteTimestamp, + id: id ?? this.id, + userStates: userStates ?? Map.from(this.userStates), + messageType: messageType ?? this.messageType, + messageContentType: messageContentType ?? this.messageContentType, + localId: localId ?? this.localId, + isForward: isForward ?? this.isForward, + isPinned: isPinned ?? this.isPinned, + isAnnouncement: isAnnouncement ?? this.isAnnouncement, + threadMessages: threadMessages ?? this.threadMessages, + parentMessage: parentMessage ?? this.parentMessage, + isEdited: isEdited ?? this.isEdited, + ); } Map toMap({bool forSender = false}) { @@ -177,7 +172,6 @@ class MessageModel { 'timestamp': timestamp.toIso8601String(), 'autoDeleteTimestamp': autoDeleteTimestamp?.microsecondsSinceEpoch, 'id': id, - 'photo': photo, 'userStates': forSender ? userStates.map( (key, value) => MapEntry(key, value.toString().split('.').last)) @@ -187,37 +181,41 @@ class MessageModel { 'localId': localId, 'isForward': isForward, 'isPinned': isPinned, + 'isEdited': isEdited, + 'isAnnouncement': isAnnouncement, + 'threadMessages': threadMessages, + 'parentMessage': parentMessage, }; return map; } static Future fromMap(Map map) async { - final message = MessageModel( - senderId: map['senderId'] as String, - content: map['content'] as MessageContent?, - timestamp: DateTime.parse(map['timestamp'] as String), - id: map['messageId'] as String?, - photo: map['photo'] as String?, - messageType: MessageType.getType(map['messageType']), - messageContentType: MessageContentType.getType(map['messageContentType']), - autoDeleteTimestamp: map['autoDeleteTimeStamp'] != null - ? DateTime.parse(map['autoDeleteTimeStamp']) - : null, - userStates: (map['userStates'] as Map?)!.map( - (key, value) => MapEntry( - key, - MessageState.values.firstWhere( - (e) => e.toString().split('.').last == value, - orElse: () => MessageState.sent, + return MessageModel( + senderId: map['senderId'] as String, + content: map['content'] as MessageContent?, + timestamp: DateTime.parse(map['timestamp'] as String), + id: map['messageId'] as String?, + messageType: MessageType.getType(map['messageType']), + messageContentType: + MessageContentType.getType(map['messageContentType']), + autoDeleteTimestamp: map['autoDeleteTimeStamp'] != null + ? DateTime.parse(map['autoDeleteTimeStamp']) + : null, + userStates: (map['userStates'] as Map?)!.map( + (key, value) => MapEntry( + key, + MessageState.values.firstWhere( + (e) => e.toString().split('.').last == value, + orElse: () => MessageState.sent, + ), ), ), - ), - isForward: map['isForward'] ?? false, - isPinned: map['isPinned'] ?? false, - ); - await message._setPhotoBytes(); - return message; + isForward: map['isForward'] ?? false, + isPinned: map['isPinned'] ?? false, + isAnnouncement: map['isAnnouncement'] ?? false, + isEdited: map['isEdited'] ?? false, + parentMessage: map['parentMessageId'] ?? ''); } String toJson({bool forSender = false}) => diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index d9196b15..cd1b22b8 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -18,7 +18,10 @@ import 'package:telware_cross_platform/features/chat/view/screens/caption_screen import 'package:telware_cross_platform/features/chat/view/screens/chat_info_screen.dart'; import 'package:telware_cross_platform/features/chat/view/screens/chat_screen.dart'; import 'package:telware_cross_platform/features/chat/view/screens/create_chat_screen.dart'; -import 'package:telware_cross_platform/features/chat/view/screens/create_group_screen.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/members_screen.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/add_members_screen.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/create_group_screen.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/group_creation_details.dart'; import 'package:telware_cross_platform/features/home/view/screens/home_screen.dart'; import 'package:telware_cross_platform/features/home/view/screens/inbox_screen.dart'; import 'package:telware_cross_platform/features/stories/view/screens/add_my_image_screen.dart'; @@ -40,8 +43,10 @@ import 'package:telware_cross_platform/features/user/view/screens/settings_scree import 'package:telware_cross_platform/features/user/view/screens/user_profile_screen.dart'; import '../../features/chat/view/screens/pinned_messages_screen.dart'; +import '../../features/groups/view/screens/edit_group.dart'; import '../../features/stories/view/screens/crop_image_screen.dart'; import '../../features/user/view/screens/devices_screen.dart'; +import '../models/user_model.dart'; class Routes { static const String home = HomeScreen.route; @@ -77,8 +82,12 @@ class Routes { static const String createChatScreen = CreateChatScreen.route; static const String chatInfoScreen = ChatInfoScreen.route; static const String createGroupScreen = CreateGroupScreen.route; + static const String groupCreationDetails = GroupCreationDetails.route; static const String callScreen = CallScreen.route; static const String captionScreen = CaptionScreen.route; + static const String editGroupScreen = EditGroup.route; + static const String addMembersScreen = AddMembersScreen.route; + static const String membersScreen = MembersScreen.route; static GoRouter appRouter(WidgetRef ref) => GoRouter( initialLocation: Routes.splash, @@ -284,6 +293,13 @@ class Routes { path: Routes.createGroupScreen, builder: (context, state) => const CreateGroupScreen(), ), + GoRoute( + path: Routes.groupCreationDetails, + builder: (context, state) { + final List members = state.extra as List; + return GroupCreationDetails(members: members); + } + ), GoRoute( path: Routes.callScreen, builder: (context, state) { @@ -306,6 +322,27 @@ class Routes { ); }, ), + GoRoute( + path: Routes.editGroupScreen, + builder: (context, state) { + final ChatModel chat = state.extra as ChatModel; + return EditGroup(chatModel: chat); + }, + ), + GoRoute( + path: Routes.addMembersScreen, + builder: (context, state) { + final String chat = state.extra as String; + return AddMembersScreen(chatId: chat); + }, + ), + GoRoute( + path: Routes.membersScreen, + builder: (context, state) { + final ChatModel chat = state.extra as ChatModel; + return MembersScreen(chatModel: chat,); + }, + ), ], ); diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 86c449fe..e914b2ea 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:socket_io_client/socket_io_client.dart'; import 'package:flutter/foundation.dart'; import 'package:telware_cross_platform/core/constants/server_constants.dart'; +import 'package:telware_cross_platform/core/mock/constants_mock.dart'; import 'package:telware_cross_platform/features/chat/view_model/event_handler.dart'; class SocketService { @@ -12,6 +13,7 @@ class SocketService { late Function() _onConnect; late EventHandler _eventHandler; bool isConnected = false, _isReconnecting = false; + Timer? timer1, timer2; final Duration _retryDelay = Duration(seconds: SOCKET_RECONNECT_DELAY_SECONDS); @@ -28,11 +30,16 @@ class SocketService { _onConnect = onConnect ?? _onConnect; _eventHandler = eventHandler ?? _eventHandler; + isConnected = false; + _isReconnecting = false; + timer1?.cancel(); + timer2?.cancel(); + _connect(); } void _connect() { - if (isConnected) return; + if (isConnected || USE_MOCK_DATA) return; debugPrint('*** Entered the connect method'); debugPrint(_serverUrl); debugPrint(_userId); @@ -45,6 +52,8 @@ class SocketService { 'auth': {'sessionId': _sessionId} }); + _socket.io.options?['debug'] = true; // Enable debug logs + _socket.connect(); _socket.onConnect((_) { @@ -56,36 +65,46 @@ class SocketService { }); _socket.onConnectError((error) { - debugPrint('Connection error: $error'); + debugPrint('### Connection error: $error'); + isConnected = false; onError(); }); _socket.onError((error) { - debugPrint('Socket error: $error'); + debugPrint('### Socket error: $error'); + isConnected = false; onError(); }); _socket.onDisconnect((_) { debugPrint('Disconnected from server'); + isConnected = false; + _isReconnecting = false; }); } void onError() { - _socket.disconnect(); - isConnected = false; + if (isConnected) return; + disconnect(); _eventHandler.stopProcessing(); if (_isReconnecting) return; _isReconnecting = true; - Timer(_retryDelay, () { + timer1 = Timer(_retryDelay, () { _isReconnecting = false; _connect(); + timer2 = Timer(_retryDelay, () { + _isReconnecting = false; + onError(); + }); }); } - void _disconnect() { + void disconnect() { _socket.disconnect(); + _socket.destroy(); isConnected = false; + debugPrint('### Socket connection destroyed'); } void on(String event, Function(dynamic data) callback) { diff --git a/lib/core/utils.dart b/lib/core/utils.dart index e8aac6cb..63b186c7 100644 --- a/lib/core/utils.dart +++ b/lib/core/utils.dart @@ -301,3 +301,54 @@ void vibrate() { } }); } + +String getRandomLottieAnimation() { + // List of Lottie animation paths + List lottieAnimations = [ + 'assets/tgs/curious_pigeon.tgs', + 'assets/tgs/fruity_king.tgs', + 'assets/tgs/graceful_elmo.tgs', + 'assets/tgs/hello_anteater.tgs', + 'assets/tgs/hello_astronaut.tgs', + 'assets/tgs/hello_badger.tgs', + 'assets/tgs/hello_bee.tgs', + 'assets/tgs/hello_cat.tgs', + 'assets/tgs/hello_clouds.tgs', + 'assets/tgs/hello_duck.tgs', + 'assets/tgs/hello_elmo.tgs', + 'assets/tgs/hello_fish.tgs', + 'assets/tgs/hello_flower.tgs', + 'assets/tgs/hello_food.tgs', + 'assets/tgs/hello_fridge.tgs', + 'assets/tgs/hello_ghoul.tgs', + 'assets/tgs/hello_king.tgs', + 'assets/tgs/hello_lama.tgs', + 'assets/tgs/hello_monkey.tgs', + 'assets/tgs/hello_pigeon.tgs', + 'assets/tgs/hello_possum.tgs', + 'assets/tgs/hello_rat.tgs', + 'assets/tgs/hello_seal.tgs', + 'assets/tgs/hello_shawn_sheep.tgs', + 'assets/tgs/hello_snail_rabbit.tgs', + 'assets/tgs/hello_virus.tgs', + 'assets/tgs/hello_water_animal.tgs', + 'assets/tgs/hello_whales.tgs', + 'assets/tgs/muscles_wizard.tgs', + 'assets/tgs/plague_doctor.tgs', + 'assets/tgs/screaming_elmo.tgs', + 'assets/tgs/shy_elmo.tgs', + 'assets/tgs/sick_wizard.tgs', + 'assets/tgs/snowman.tgs', + 'assets/tgs/spinny_jelly.tgs', + 'assets/tgs/sus_moon.tgs', + 'assets/tgs/toiletpaper.tgs', + ]; + + // Generate a random index + Random random = Random(); + int randomIndex = + random.nextInt(lottieAnimations.length); // Gets a random index + + // Return the randomly chosen Lottie animation path + return lottieAnimations[randomIndex]; +} diff --git a/lib/core/view/widget/popup_menu_item_widget.dart b/lib/core/view/widget/popup_menu_item_widget.dart index ccf67c16..149d21df 100644 --- a/lib/core/view/widget/popup_menu_item_widget.dart +++ b/lib/core/view/widget/popup_menu_item_widget.dart @@ -23,7 +23,7 @@ class PopupMenuItemWidget extends StatelessWidget { return Container( color: Palette.secondary, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0), + padding: const EdgeInsets.only(left: 12.0, top: 6.0, bottom: 6, right: 2.0), child: Row( children: [ Icon(icon, color: color ?? Palette.accentText,), @@ -34,7 +34,6 @@ class PopupMenuItemWidget extends StatelessWidget { fontSize: 16, ), ), - const SizedBox(width: 20), Expanded( child: Align( alignment: Alignment.centerRight, diff --git a/lib/core/view/widget/popup_menu_widget.dart b/lib/core/view/widget/popup_menu_widget.dart index 2f8f26cd..e9270728 100644 --- a/lib/core/view/widget/popup_menu_widget.dart +++ b/lib/core/view/widget/popup_menu_widget.dart @@ -14,16 +14,17 @@ class PopupMenuWidget extends StatelessWidget { static void showPopupMenu({ required BuildContext context, + Offset? position, required List items, required Function onSelected, }) { final RenderBox renderBox = context.findRenderObject() as RenderBox; - final position = renderBox.localToGlobal(Offset.zero); // Get position on screen + position ??= renderBox.localToGlobal(Offset.zero); showMenu( context: context, color: Palette.secondary, - position: RelativeRect.fromLTRB(position.dx, position.dy + renderBox.size.height / 2, position.dx + 100, position.dy), + position: RelativeRect.fromLTRB(position.dx, position.dy - renderBox.size.height, position.dx + 100, position.dy), items: >[ ...items.map((item) { return PopupMenuItem( diff --git a/lib/features/auth/repository/auth_remote_repository.dart b/lib/features/auth/repository/auth_remote_repository.dart index a1b5cc58..063de17c 100644 --- a/lib/features/auth/repository/auth_remote_repository.dart +++ b/lib/features/auth/repository/auth_remote_repository.dart @@ -241,7 +241,7 @@ class AuthRemoteRepository { int? code; if (dioException.response != null) { code = dioException.response!.statusCode; - debugPrint(code.toString()); + debugPrint('^&^ get me error with status: $code'); message = dioException.response!.data?['message'] ?? 'Unexpected server Error'; debugPrint(message); diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart index 7a115938..b1d7bd35 100644 --- a/lib/features/auth/view_model/auth_view_model.dart +++ b/lib/features/auth/view_model/auth_view_model.dart @@ -53,6 +53,10 @@ class AuthViewModel extends _$AuthViewModel { response.match((appError) { state = AuthState.fail(appError.error); + if (appError.code == 401) { + _handleLogOutState(); + return; + } // getting user data from local as remote failed final user = ref.read(authLocalRepositoryProvider).getMe(); ref.read(userProvider.notifier).update((_) => user); @@ -294,7 +298,7 @@ class AuthViewModel extends _$AuthViewModel { debugPrint('==============================='); debugPrint('log out operation ended'); debugPrint('Error: ${appError?.error}'); - await _handleLogOutState(appError); + await _handleLogOutState(); } Future logOutAllOthers() async { @@ -313,25 +317,21 @@ class AuthViewModel extends _$AuthViewModel { state = AuthState.loading; final token = ref.read(tokenProvider); - final appError = await ref + await ref .read(authRemoteRepositoryProvider) .logOut(token: token!, route: 'auth/logout-all'); - await _handleLogOutState(appError); + await _handleLogOutState(); } - Future _handleLogOutState(AppError? appError) async { - if (appError == null) { - // successful log out operation - await ref.read(authLocalRepositoryProvider).deleteToken(); - ref.read(tokenProvider.notifier).update((_) => null); + Future _handleLogOutState() async { + // successful log out operation + await ref.read(authLocalRepositoryProvider).deleteToken(); + ref.read(tokenProvider.notifier).update((_) => null); - await ref.read(authLocalRepositoryProvider).deleteUser(); - ref.read(userProvider.notifier).update((_) => null); - state = AuthState.unauthenticated; - } else { - state = AuthState.fail(appError.error); - } + await ref.read(authLocalRepositoryProvider).deleteUser(); + ref.read(userProvider.notifier).update((_) => null); + state = AuthState.unauthenticated; } Future getMe() async { @@ -347,7 +347,11 @@ class AuthViewModel extends _$AuthViewModel { // try getting updated user data final response = await ref.read(authRemoteRepositoryProvider).getMe(token!); - response.match((appError) {}, (user) async { + response.match((appError) { + if (appError.code == 401) { + _handleLogOutState(); + } + }, (user) async { debugPrint('** getMe is called\nuser supposed to have img'); debugPrint(user.toString()); ref.read(authLocalRepositoryProvider).setUser(user); diff --git a/lib/features/chat/classes/message_content.dart b/lib/features/chat/classes/message_content.dart index fc3c05b2..3589f9e4 100644 --- a/lib/features/chat/classes/message_content.dart +++ b/lib/features/chat/classes/message_content.dart @@ -17,6 +17,7 @@ abstract class MessageContent { MessageContent copyWith(); String getContent(); + String? getMediaURL(); } // For Text Messages @@ -43,6 +44,11 @@ class TextContent extends MessageContent { String getContent() { return text; } + + @override + String? getMediaURL() { + return null; + } } // For Audio Messages @@ -91,6 +97,11 @@ class AudioContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return audioUrl; + } } // For Document Messages (PDFs, Docs) @@ -122,6 +133,11 @@ class DocumentContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return fileUrl; + } } // For Image Messages @@ -160,6 +176,11 @@ class ImageContent extends MessageContent { String getContent() { return caption ?? ''; } + + @override + String? getMediaURL() { + return imageUrl; + } } // For Video Messages @@ -197,6 +218,11 @@ class VideoContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return videoUrl; + } } // for emoji, gifs and stickers @@ -228,6 +254,11 @@ class EmojiContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return emojiUrl; + } } @HiveType(typeId: 21) @@ -258,6 +289,11 @@ class GIFContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return gifUrl; + } } @HiveType(typeId: 22) @@ -288,4 +324,9 @@ class StickerContent extends MessageContent { String getContent() { return fileName ?? ''; } + + @override + String? getMediaURL() { + return stickerUrl; + } } diff --git a/lib/features/chat/enum/chatting_enums.dart b/lib/features/chat/enum/chatting_enums.dart index 8e6239c2..d801ca1e 100644 --- a/lib/features/chat/enum/chatting_enums.dart +++ b/lib/features/chat/enum/chatting_enums.dart @@ -4,9 +4,14 @@ part 'chatting_enums.g.dart'; enum EventType { sendMessage(event: 'SEND_MESSAGE'), + pinMessageClient(event: 'PIN_MESSAGE_CLIENT'), + unpinMessageClient(event: 'UNPIN_MESSAGE_CLIENT'), + pinMessageServer(event: 'PIN_MESSAGE_SERVER'), + unpinMessageServer(event: 'UNPIN_MESSAGE_SERVER'), sendAnnouncement(event: 'SEND_ANNOUNCEMENT'), updateDraft(event: 'UPDATE_DRAFT'), - editMessage(event: 'EDIT_MESSAGE'), + editMessageClient(event: 'EDIT_MESSAGE_CLIENT'), + editMessageServer(event: 'EDIT_MESSAGE_SERVER'), deleteMessage(event: 'DELETE_MESSAGE'), replyOnMessage(event: 'REPLY_MESSAGE'), forwardMessage(event: 'FORWARD_MESSAGE'), @@ -14,6 +19,28 @@ enum EventType { ////////////////////////////// receiveMessage(event: 'RECEIVE_MESSAGE'), receiveReply(event: 'RECEIVE_REPLY'), + + createGroup(event: 'CREATE_GROUP_CHANNEL'), + receiveCreateGroup(event: 'JOIN_GROUP_CHANNEL'), + + leaveGroup(event: 'LEAVE_GROUP_CHANNEL_CLIENT'), + receiveLeaveGroup(event: 'LEAVE_GROUP_CHANNEL_SERVER'), + + deleteGroup(event: 'DELETE_GROUP_CHANNEL_CLIENT'), + receiveDeleteGroup(event: 'DELETE_GROUP_CHANNEL_SERVER'), + + addMember(event: 'ADD_MEMBERS_CLIENT'), + receiveAddMember(event: 'ADD_MEMBERS_SERVER'), + + addAdmin(event: 'ADD_ADMINS_CLIENT'), + receiveAddAdmin(event: 'ADD_ADMIN_SERVER'), + + removeMember(event: 'REMOVE_MEMBERS_CLIENT'), + receiveRemoveMember(event: 'REMOVE_MEMBERS_SERVER'), + + setPermissions(event: 'SET_PERMISSION_CLIENT'), + receiveSetPermissions(event: 'SET_PERMISSION_SERVER'), + ; final String event; diff --git a/lib/features/chat/models/message_event_models.dart b/lib/features/chat/models/message_event_models.dart index 50e1dc38..11d11422 100644 --- a/lib/features/chat/models/message_event_models.dart +++ b/lib/features/chat/models/message_event_models.dart @@ -31,18 +31,56 @@ class MessageEvent { @HiveField(2) final String chatId; + final ChattingController? _controller; + static const int _timeOutSeconds = 10; + final Function(Map res)? _onEventComplete; MessageEvent( this.payload, { required this.msgId, required this.chatId, + Function(Map res)? onEventComplete, ChattingController? controller, - }) : _controller = controller; + }) : _controller = controller, _onEventComplete = onEventComplete; Future execute(SocketService socket, - {Duration timeout = const Duration(seconds: 10)}) async { - debugPrint('!!! this is the one excuted'); + {Duration timeout = const Duration(seconds: _timeOutSeconds), + }) async { + final success = await _execute( + socket, + 'eventName', // Event name for the socket message + timeout: timeout, + ackCallback: (response, timer, completer) { + // Handle the acknowledgment response and provide feedback + if (response != null) { + if(_onEventComplete == null){ + print('fdsafasd'); + } + else{ + _onEventComplete({}); + } + completer.complete(true); + } else { + if(_onEventComplete == null){ + print('fdsafasd'); + } + else { + _onEventComplete({}); + } + completer.complete(false); + } + }, + ); + + if (!success) { + if(_onEventComplete == null){ + print('fdsafasd'); + } + else { + _onEventComplete({}); + } + } return true; } @@ -76,12 +114,14 @@ class MessageEvent { ChattingController? controller, String? msgId, String? chatId, + Function(Map res)? onEventComplete, }) { return MessageEvent( payload ?? this.payload, controller: controller ?? _controller, msgId: msgId ?? this.msgId, chatId: chatId ?? this.chatId, + onEventComplete: onEventComplete ?? _onEventComplete, ); } } @@ -89,20 +129,17 @@ class MessageEvent { @HiveType(typeId: 8) class SendMessageEvent extends MessageEvent { SendMessageEvent(super.payload, - {super.controller, required super.msgId, required super.chatId}); + {super.controller, required super.msgId, required super.chatId, required super.onEventComplete}); @override Future execute( SocketService socket, { - Duration timeout = const Duration(seconds: 10), + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), }) async { - debugPrint('!!! Sending event statrted'); - print(payload as Map); - debugPrint('--- did not reach here in sending event'); - return await _execute( socket, EventType.sendMessage.event, + timeout: timeout, ackCallback: (res, timer, completer) { try { final response = res as Map; @@ -140,12 +177,13 @@ class SendMessageEvent extends MessageEvent { ChattingController? controller, String? msgId, String? chatId, + Function(Map res)? onEventComplete, }) { return SendMessageEvent( payload ?? this.payload, controller: controller ?? _controller, msgId: msgId ?? this.msgId, - chatId: chatId ?? this.chatId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, ); } } @@ -156,21 +194,27 @@ class DeleteMessageEvent extends MessageEvent { super.payload, { super.controller, required super.msgId, - required super.chatId, + required super.chatId, required super.onEventComplete, }); @override Future execute( SocketService socket, { Duration timeout = const Duration(seconds: 10), - }) async { + }) async { return await _execute( socket, EventType.deleteMessage.event, ackCallback: (response, timer, completer) { if (!completer.isCompleted) { - timer.cancel(); // Cancel the timeout timer - completer.complete(true); + timer.cancel(); // Cancel the timer on acknowledgment + if (response['success'].toString() == 'true') { + debugPrint('&()& delete msg sucessefully'); + completer.complete(true); + } else { + debugPrint('&()& delete msg failed'); + completer.complete(false); + } } }, ); @@ -182,12 +226,14 @@ class DeleteMessageEvent extends MessageEvent { ChattingController? controller, String? msgId, String? chatId, + Function(Map res)? onEventComplete, + }) { return DeleteMessageEvent( payload ?? this.payload, controller: controller ?? _controller, msgId: msgId ?? this.msgId, - chatId: chatId ?? this.chatId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, ); } } @@ -198,21 +244,41 @@ class EditMessageEvent extends MessageEvent { super.payload, { super.controller, required super.msgId, - required super.chatId, + required super.chatId, required super.onEventComplete, }); @override Future execute( SocketService socket, { Duration timeout = const Duration(seconds: 10), - }) async { + + }) async { return await _execute( socket, - EventType.deleteMessage.event, - ackCallback: (response, timer, completer) { - if (!completer.isCompleted) { - timer.cancel(); // Cancel the timeout timer - completer.complete(true); + EventType.editMessageClient.event, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + if (!completer.isCompleted) { + timer.cancel(); // Cancel the timer on acknowledgment + if (response['success'].toString() == 'true') { + final res = response['res']['message'] as Map; + + _controller!.editMessageIdAck( + msgId: res['_id'] ?? res['id'] ?? msgId, + content: res['content'], + chatId: res['chatId'] ?? chatId, + ); + + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement of the edit'); + debugPrint(e.toString()); + completer.complete(false); } }, ); @@ -224,30 +290,33 @@ class EditMessageEvent extends MessageEvent { ChattingController? controller, String? msgId, String? chatId, + Function(Map res)? onEventComplete, + }) { return EditMessageEvent( payload ?? this.payload, controller: controller ?? _controller, msgId: msgId ?? this.msgId, - chatId: chatId ?? this.chatId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, ); } } -@HiveType(typeId: 11) +@HiveType(typeId: 23) class UpdateDraftEvent extends MessageEvent { UpdateDraftEvent( super.payload, { super.controller, required super.msgId, - required super.chatId, + required super.chatId, required super.onEventComplete, }); @override Future execute( SocketService socket, { Duration timeout = const Duration(seconds: 10), - }) async { + + }) async { return await _execute( socket, EventType.updateDraft.event, @@ -272,12 +341,558 @@ class UpdateDraftEvent extends MessageEvent { ChattingController? controller, String? msgId, String? chatId, + Function(Map res)? onEventComplete, + }) { return UpdateDraftEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 24) +class PinMessageEvent extends MessageEvent { + PinMessageEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required this.isToPin, required super.onEventComplete, + }); + + @HiveField(3) + bool isToPin; + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + + }) async { + final event = isToPin + ? EventType.pinMessageClient.event + : EventType.unpinMessageClient.event; + socket.emit(event, payload); + return true; + } + + @override + PinMessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return PinMessageEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, + isToPin: isToPin ?? this.isToPin, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 25) +class CreateGroupEvent extends MessageEvent { + CreateGroupEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.createGroup.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); // Cancel the timer on acknowledgment + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + if(_onEventComplete != null){ + _onEventComplete(response); + } + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + CreateGroupEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return CreateGroupEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 26) +class DeleteGroupEvent extends MessageEvent { + DeleteGroupEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.deleteGroup.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + CreateGroupEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return CreateGroupEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 27) +class LeaveGroupEvent extends MessageEvent { + LeaveGroupEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.leaveGroup.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + LeaveGroupEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return LeaveGroupEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 28) +class AddMembersEvent extends MessageEvent { + AddMembersEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.addMember.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + AddMembersEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return AddMembersEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 29) +class AddAdminEvent extends MessageEvent { + AddAdminEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.addAdmin.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + AddAdminEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return AddAdminEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 30) +class RemoveMemberEvent extends MessageEvent { + RemoveMemberEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.removeMember.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + RemoveMemberEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return RemoveMemberEvent( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 31) +class SetPermissions extends MessageEvent { + SetPermissions( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required super.onEventComplete, + }); + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + return await _execute( + socket, + EventType.setPermissions.event, + timeout: timeout, + ackCallback: (res, timer, completer) { + try { + final response = res as Map; + debugPrint('### I got a response ${response['success'].toString()}'); + print(response); + if (!completer.isCompleted) { + timer.cancel(); + if(_onEventComplete != null){ + _onEventComplete(response); + } + if (response['success'].toString() == 'true') { + final res = response['data'] as Map; + print(res.toString()); + _controller?.getUserChats(); + completer.complete(true); + } else { + completer.complete(false); + } + } + } catch (e) { + debugPrint('--- Error in processing the acknowledgement'); + debugPrint(e.toString()); + } + }, + ); + } + + @override + SetPermissions copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + Function(Map res)? onEventComplete, + + }) { + return SetPermissions( + payload ?? this.payload, + controller: controller ?? _controller, + msgId: msgId ?? this.msgId, + chatId: chatId ?? this.chatId, onEventComplete: onEventComplete ?? _onEventComplete, + ); + } +} + +@HiveType(typeId: 24) +class PinMessageEvent extends MessageEvent { + PinMessageEvent( + super.payload, { + super.controller, + required super.msgId, + required super.chatId, + required this.isToPin, + }); + + @HiveField(3) + bool isToPin; + + @override + Future execute( + SocketService socket, { + Duration timeout = const Duration(seconds: MessageEvent._timeOutSeconds), + }) async { + final event = isToPin + ? EventType.pinMessageClient.event + : EventType.unpinMessageClient.event; + socket.emit(event, payload); + return true; + } + + @override + PinMessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + String? msgId, + String? chatId, + bool? isToPin, + }) { + return PinMessageEvent( payload ?? this.payload, controller: controller ?? _controller, msgId: msgId ?? this.msgId, chatId: chatId ?? this.chatId, + isToPin: isToPin ?? this.isToPin, ); } } diff --git a/lib/features/chat/repository/chat_local_repository.dart b/lib/features/chat/repository/chat_local_repository.dart index 1d40090c..5e38a99f 100644 --- a/lib/features/chat/repository/chat_local_repository.dart +++ b/lib/features/chat/repository/chat_local_repository.dart @@ -51,6 +51,10 @@ class ChatLocalRepository { return chats; } + Future clearChats(String userId) async { + await _chatsBox.delete(_chatsBoxKey+userId); + } + ///////////////////////////////////// // get other users Future setOtherUsers(Map otherUsers, String userId) async { @@ -76,6 +80,10 @@ class ChatLocalRepository { return otherUsersMap; } + void clearOtherUsers(String userId) async { + await _otherUsersBox.delete(_otherUsersBoxKey+userId); + } + ///////////////////////////////////// // sets event queue Future setEventQueue(Queue queue, String userId) async { @@ -102,4 +110,8 @@ class ChatLocalRepository { final queue = Queue.from(eventsList); return queue; } + + Future clearEventQueue(String userId) async { + await _eventsBox.delete(_eventsBoxKey+userId); + } } diff --git a/lib/features/chat/repository/chat_remote_repository.dart b/lib/features/chat/repository/chat_remote_repository.dart index be2e1c8f..0c36756e 100644 --- a/lib/features/chat/repository/chat_remote_repository.dart +++ b/lib/features/chat/repository/chat_remote_repository.dart @@ -18,10 +18,9 @@ class ChatRemoteRepository { ({ AppError? appError, List chats, - List users, + Map users, })> getUserChats(String sessionId, String userID) async { List chats = []; - List users = []; try { final response = await _dio.get( @@ -87,7 +86,9 @@ class ChatRemoteRepository { mediaUrl: lastMessage['mediaUrl'], ); - // todo(ahmed): add (isForward, isPinned, isAnnouncement, parentMsgId) + final threadMessages = (lastMessage['threadMessages'] as List).map((e) => e as String).toList(); + + // todo(ahmed): add (parentMsgId) // the connumicationType attribute is extra lastMessageMap[message['chatId']] = MessageModel( id: lastMessage['id'], @@ -99,6 +100,10 @@ class ChatRemoteRepository { ? DateTime.parse(lastMessage['timestamp']) : DateTime.now(), userStates: userStates, + isForward: lastMessage['isForward'] ?? false, + isPinned: lastMessage['isPinned'] ?? false, + isAnnouncement: lastMessage['isAnnouncement'], + threadMessages: threadMessages ); } @@ -128,23 +133,32 @@ class ChatRemoteRepository { lastMessageMap[chatID] == null ? [] : [lastMessageMap[chatID]!]; String chatTitle = 'Invalid Chat'; + bool messagingPermission = true; if (chat['chat']['type'] == 'private') { if (otherUsers.isEmpty) { continue; } - chatTitle = otherUsers[0]?.username ?? 'Private Chat'; - } else if (chat['chat']['type'] == 'group') { - chatTitle = 'Group Chat'; + + chatTitle = + '${otherUsers[0]?.screenFirstName} ${otherUsers[0]?.screenLastName}'; + if (chatTitle.isEmpty) { + chatTitle = (otherUsers[0]?.username != null && + otherUsers[0]!.username.isNotEmpty) + ? otherUsers[0]!.username + : 'Private Chat'; + } + } + else if (chat['chat']['type'] == 'group') { + chatTitle = chat['chat']['name'] ?? 'Group Chat'; + messagingPermission = chat['chat']['messagingPermission']; } else if (chat['chat']['type'] == 'channel') { - chatTitle = 'Channel'; + chatTitle = chat['chat']['name'] ?? 'Channel'; + messagingPermission = chat['chat']['messagingPermission']; } else { - chatTitle = 'My Chat'; - chatTitle = 'My Chat'; + chatTitle = 'Error in chat'; } - // Create chat model - // Contains the last message only - // todo(ahmed): store the creators list as well as the isMuted, isSeen, isDeleted and draft attributes + // todo(ahmed): store the creators list as well as the isSeen, isDeleted attributes // should it be sent in the first place if it is deleted? final chatModel = ChatModel( id: chatID, @@ -155,20 +169,21 @@ class ChatRemoteRepository { messages: messages, draft: chat['draft'], isMuted: chat['isMuted'], + creators: creators, + messagingPermission: messagingPermission ); chats.add(chatModel); - users.addAll(otherUsers.whereType()); } - return (chats: chats, users: users, appError: null); + return (chats: chats, users: userMap, appError: null); } catch (e, stackTrace) { debugPrint('!!! error in recieving the chats'); debugPrint(e.toString()); debugPrint(stackTrace.toString()); return ( chats: [], - users: [], + users: {}, appError: AppError('Failed to fetch chats', code: 500), ); } @@ -209,8 +224,9 @@ class ChatRemoteRepository { id: data['id'] ?? 'unknown_id', ); } catch (e, stackTrace) { - debugPrint('Failed to fetch user details: ${e.toString()}'); - debugPrint('Stack trace: $stackTrace'); + debugPrint('Failed to fetch user details'); + // debugPrint('Failed to fetch user details: ${e.toString()}'); + // debugPrint('Stack trace: $stackTrace'); return null; } } diff --git a/lib/features/chat/view/screens/chat_info_screen.dart b/lib/features/chat/view/screens/chat_info_screen.dart index d07eb5c0..108e71a8 100644 --- a/lib/features/chat/view/screens/chat_info_screen.dart +++ b/lib/features/chat/view/screens/chat_info_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:telware_cross_platform/core/models/chat_model.dart'; import 'package:telware_cross_platform/core/models/user_model.dart'; +import 'package:telware_cross_platform/core/providers/token_provider.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/dimensions.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; @@ -16,7 +18,8 @@ import 'package:telware_cross_platform/features/auth/view/widget/title_element.d import 'package:telware_cross_platform/features/chat/view/widget/member_tile_widget.dart'; import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; -import 'package:telware_cross_platform/features/user/view/widget/avatar_generator.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/add_members_screen.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/edit_group.dart'; import 'package:telware_cross_platform/features/user/view/widget/profile_header_widget.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_toggle_switch_widget.dart'; @@ -35,12 +38,14 @@ class _ChatInfoScreen extends ConsumerState late final Future> usersInfoFuture; late ChatModel chat = widget.chatModel; late TabController _tabController; + bool showAutoDeleteOptions = false; Future> getUsersInfo() async { final ChatModel chat = widget.chatModel; final List users = []; for (final String userId in chat.userIds) { - final UserModel? user = await ref.read(chatsViewModelProvider.notifier).getUser(userId); + final UserModel? user = + await ref.read(chatsViewModelProvider.notifier).getUser(userId); users.add(user); } return users; @@ -61,19 +66,17 @@ class _ChatInfoScreen extends ConsumerState void _toggleMute(bool isMuted) { if (!isMuted) { - ref.read(chattingControllerProvider).muteChat(chat, null) - .then((_) => { - setState(() { - chat = chat.copyWith(isMuted: true, muteUntil: null); - }) - }); + ref.read(chattingControllerProvider).muteChat(chat, null).then((_) => { + setState(() { + chat = chat.copyWith(isMuted: true, muteUntil: null); + }) + }); } else { - ref.read(chattingControllerProvider).unmuteChat(chat) - .then((_) => { - setState(() { - chat = chat.copyWith(isMuted: false, muteUntil: null); - }) - }); + ref.read(chattingControllerProvider).unmuteChat(chat).then((_) => { + setState(() { + chat = chat.copyWith(isMuted: false, muteUntil: null); + }) + }); } } @@ -85,8 +88,7 @@ class _ChatInfoScreen extends ConsumerState }); }); } else { - ref.read(chattingControllerProvider).muteChat(chat, muteUntil) - .then((_) { + ref.read(chattingControllerProvider).muteChat(chat, muteUntil).then((_) { setState(() { chat = chat.copyWith(isMuted: true, muteUntil: muteUntil); }); @@ -94,73 +96,326 @@ class _ChatInfoScreen extends ConsumerState } } - void _showNotificationSettings(BuildContext context) { - var items = !chat.isMuted ? [ - {'icon': Icons.music_off_outlined, 'text': 'Disable sound', 'value': 'disable-sound'}, - {'icon': Icons.access_time_rounded, 'text': 'Mute for 30m', 'value': 'mute-30m'}, - {'icon': Icons.notifications_paused_outlined, 'text': 'Mute for...', 'value': 'mute-custom'}, - {'icon': Icons.tune_outlined, 'text': 'Customize', 'value': 'customize'}, - {'icon': Icons.volume_off_outlined, 'text': 'Mute Forever', 'value': 'mute-forever', 'color': Palette.error}, - ] : [ - {'icon': Icons.notifications_paused_outlined, 'text': 'Mute for...', 'value': 'mute-custom'}, - {'icon': Icons.tune_outlined, 'text': 'Customize', 'value': 'customize'}, - {'icon': Icons.volume_up_outlined, 'text': 'Unmute', 'value': 'unmute', 'color': Palette.valid}, - ]; + void _confirmDelete(BuildContext context) { + bool isChecked = false; - PopupMenuWidget.showPopupMenu( - context: context, - items: items, - onSelected: (value) { - switch (value) { - case 'mute-30m': - _setChatMute(true, DateTime.now().add(const Duration(minutes: 30))); - break; - case 'mute-custom': - DatePicker.showDatePicker( - context, - pickerTheme: const DateTimePickerTheme( - backgroundColor: Palette.secondary, - itemTextStyle: TextStyle( - color: Palette.primaryText, - fontSize: 20, - fontWeight: FontWeight.w600, + showDialog( + context: context, + builder: (BuildContext context) { + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return AlertDialog( + insetPadding: EdgeInsets.symmetric(horizontal: 10), + backgroundColor: Palette.secondary, + title: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundImage: widget.chatModel.photoBytes != null + ? MemoryImage(widget.chatModel.photoBytes!) + : null, + backgroundColor: widget.chatModel.photoBytes == null + ? getRandomColor(chat.title) + : null, + child: widget.chatModel.photoBytes == null + ? Text( + getInitials(chat.title), + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Palette.primaryText, + ), + ) + : null, ), - confirm: Text( - 'Confirm', + const SizedBox(width: 10), + const Text( + 'Delete group', style: TextStyle( - color: Palette.primary, - fontSize: 18, - fontWeight: FontWeight.w500, + fontSize: 14, + fontWeight: FontWeight.bold, + color: Palette.primaryText, ), ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Are you sure you want to delete and leave this group?", + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 15, + ), + ), + const SizedBox(height: 10), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Checkbox( + value: isChecked, + onChanged: (bool? value) { + setState(() { + isChecked = value ?? false; + }); + }, + ), + Expanded( + child: Text( + "Delete the group for all members", + style: TextStyle(color: Palette.primaryText), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + child: Text( + "Cancel", + style: TextStyle(color: Palette.primary), + ), ), - pickerMode: DateTimePickerMode.time, - minDateTime: DateTime.now(), - maxDateTime: DateTime.now().add(const Duration(days: 365)), - initialDateTime: DateTime.now(), - dateFormat: 'dd-MMMM-yyyy', - locale: DateTimePickerLocale.en_us, - onConfirm: (date, time) { - _setChatMute(true, date); - }, - ); - break; - case 'mute-forever': - _setChatMute(true, null); - break; - case 'unmute': - _setChatMute(false, null); - break; - default: - showToastMessage('Coming soon'); - } - } + TextButton( + onPressed: () async { + _callSockets(isChecked); + }, + child: Text( + "Delete Group", + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + }, + ); + }, ); } + Future _callSockets(bool isChecked) async { + if (isChecked) { + await ref.read(chattingControllerProvider).deleteGroup( + chatId: chat.id ?? '', + onEventComplete: (res) async { + if (res['success'] == true) { + debugPrint('Group deleted successfully'); + } else { + debugPrint('Failed to delete group try again later'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to delete group try again later'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + } else { + await ref.read(chattingControllerProvider).leaveGroup( + chatId: chat.id ?? '', + onEventComplete: (res) async { + if (res['success'] == true) { + debugPrint('Group deleted successfully'); + } else { + debugPrint('Failed to leave group try again later'); + // Show a SnackBar if group creation failed + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to leave group try again later'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + } + } + + void _showMoreSettings() { + var items = []; + if (showAutoDeleteOptions) { + items.addAll([ + {'icon': Icons.arrow_back, 'text': 'Back', 'value': 'no-close'}, + {'icon': Icons.timer_sharp, 'text': '1 day', 'value': 'auto-1d'}, + { + 'icon': Icons.access_time_rounded, + 'text': '1 week', + 'value': 'auto-1w' + }, + { + 'icon': Icons.share_arrival_time_outlined, + 'text': '1 month', + 'value': 'auto-1m' + }, + { + 'icon': Icons.tune_outlined, + 'text': 'Customize', + 'value': 'customize' + }, + { + 'icon': Icons.do_disturb_alt, + 'text': 'Disable', + 'value': 'disable-auto', + 'color': Palette.error + }, + ]); + } else { + items.addAll([ + { + 'icon': Icons.more_time, + 'text': 'Auto-Delete', + 'value': 'no-close', + 'trailing': const Icon(Icons.arrow_forward_ios, + color: Palette.inactiveSwitch, size: 16) + }, + { + 'icon': Icons.voice_chat_outlined, + 'text': 'Start Video chat', + 'value': 'video-call' + }, + {'icon': Icons.search, 'text': 'Search Members', 'value': 'search'}, + { + 'icon': Icons.logout_outlined, + 'text': 'Delete and Leave Group', + 'value': 'delete-group' + }, + { + 'icon': Icons.add_home_outlined, + 'text': 'Add to Home Screen', + 'value': 'add-home' + }, + ]); + } + + final renderBox = context.findRenderObject() as RenderBox; + final position = Offset(renderBox.size.width, -350); + + PopupMenuWidget.showPopupMenu( + context: context, + position: position, + items: items, + onSelected: _handlePopupMenuSelection); + } + + void _showNotificationSettings(BuildContext context) { + var items = !chat.isMuted + ? [ + { + 'icon': Icons.music_off_outlined, + 'text': 'Disable sound', + 'value': 'disable-sound' + }, + { + 'icon': Icons.access_time_rounded, + 'text': 'Mute for 30m', + 'value': 'mute-30m' + }, + { + 'icon': Icons.notifications_paused_outlined, + 'text': 'Mute for...', + 'value': 'mute-custom' + }, + { + 'icon': Icons.tune_outlined, + 'text': 'Customize', + 'value': 'customize' + }, + { + 'icon': Icons.volume_off_outlined, + 'text': 'Mute Forever', + 'value': 'mute-forever', + 'color': Palette.error + }, + ] + : [ + { + 'icon': Icons.notifications_paused_outlined, + 'text': 'Mute for...', + 'value': 'mute-custom' + }, + { + 'icon': Icons.tune_outlined, + 'text': 'Customize', + 'value': 'customize' + }, + { + 'icon': Icons.volume_up_outlined, + 'text': 'Unmute', + 'value': 'unmute', + 'color': Palette.valid + }, + ]; + + PopupMenuWidget.showPopupMenu( + context: context, items: items, onSelected: _handlePopupMenuSelection); + } + + void _handlePopupMenuSelection(dynamic value) { + switch (value) { + case 'no-close': + showAutoDeleteOptions = !showAutoDeleteOptions; + break; + case 'mute-30m': + _setChatMute(true, DateTime.now().add(const Duration(minutes: 30))); + break; + case 'mute-custom': + DatePicker.showDatePicker( + context, + pickerTheme: const DateTimePickerTheme( + backgroundColor: Palette.secondary, + itemTextStyle: TextStyle( + color: Palette.primaryText, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + confirm: Text( + 'Confirm', + style: TextStyle( + color: Palette.primary, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + pickerMode: DateTimePickerMode.time, + minDateTime: DateTime.now(), + maxDateTime: DateTime.now().add(const Duration(days: 365)), + initialDateTime: DateTime.now(), + dateFormat: 'dd-MMMM-yyyy', + locale: DateTimePickerLocale.en_us, + onConfirm: (date, time) { + _setChatMute(true, date); + }, + ); + break; + case 'mute-forever': + _setChatMute(true, null); + break; + case 'unmute': + _setChatMute(false, null); + break; + case 'delete-group': + _confirmDelete(context); + break; + default: + showToastMessage('Coming soon'); + } + } + void _addMembers() { - // TODO: Implement this - context.go('/add-members', extra: {'chatId': chat.id}); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddMembersScreen( + chatId: chat.id??'', + ), + ), + ); } @override @@ -183,17 +438,43 @@ class _ChatInfoScreen extends ConsumerState IconButton( icon: const Icon(Icons.edit), onPressed: () { + if(chat.admins!.contains(ref.read(userProvider)?.id)) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditGroup( + chatModel: widget.chatModel, + )), + ); + } + else{ + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Access Denied'), + content: const Text('You do not have the necessary permissions to edit this group.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); + } // context.push( - // '/edit-chat', - // extra: {'chatId': chat.id}, + // Routes.editGroupScreen, + // extra: widget.chatModel, // ); - } - ), + }), const SizedBox(width: 16), IconButton( - onPressed: () { - // Create popup menu and stuff - }, + onPressed: _showMoreSettings, icon: const Icon(Icons.more_vert)), ], flexibleSpace: LayoutBuilder( @@ -217,18 +498,17 @@ class _ChatInfoScreen extends ConsumerState ), SliverToBoxAdapter( child: Container( - color: Palette.secondary, - child: SettingsToggleSwitchWidget( - text: 'Notifications', - subtext: 'Custom', // TODO: Update this to be dynamic - isChecked: !chat.isMuted, - onToggle: _toggleMute, - onTap: _showNotificationSettings, - oneFunction: false, - showDivider: false, - ), - ) - ), + color: Palette.secondary, + child: SettingsToggleSwitchWidget( + text: 'Notifications', + subtext: 'Custom', // TODO: Update this to be dynamic + isChecked: !chat.isMuted, + onToggle: _toggleMute, + onTap: _showNotificationSettings, + oneFunction: false, + showDivider: false, + ), + )), SliverToBoxAdapter( child: Container( color: Palette.background, @@ -240,44 +520,49 @@ class _ChatInfoScreen extends ConsumerState color: Palette.secondary, child: Column( children: [ - Padding( - padding: const EdgeInsets.only(left: 22, top: 8), - child: ListTile( - leading: const Icon( - Icons.person_add_outlined, - color: Palette.primary, - ), - title: const Text( - 'Add Members', - style: TextStyle( - color: Palette.primary, - fontSize: 16, - fontWeight: FontWeight.w400, - ) - ), - onTap: _addMembers, - ), - ), + chat.admins!.contains(ref.read(userProvider)?.id) + ? Padding( + padding: const EdgeInsets.only(left: 22, top: 8), + child: ListTile( + leading: const Icon( + Icons.person_add_outlined, + color: Palette.primary, + ), + title: const Text('Add Members', + style: TextStyle( + color: Palette.primary, + fontSize: 16, + fontWeight: FontWeight.w400, + )), + onTap: _addMembers, + ), + ) + : SizedBox(), FutureBuilder( future: usersInfoFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - final List users = snapshot.data as List; + final List users = + snapshot.data as List; return Column( children: [ for (final UserModel? user in users) ...[ - Container( - color: Palette.secondary, - child: MemberTileWidget( - imagePath: user!.photo, - text: '${user.screenFirstName} ${user.screenLastName}', - subtext: user.status, - showDivider: false, - onTap: () { - context.push(Routes.userProfile, extra: user.id); - }, - ), - ), + user == null + ? const SizedBox.shrink() + : Container( + color: Palette.secondary, + child: MemberTileWidget( + imagePath: user.photo, + text: + '${user.screenFirstName} ${user.screenLastName}', + subtext: user.status, + showDivider: false, + onTap: () { + context.push(Routes.userProfile, + extra: user.id); + }, + ), + ), ], ], ); @@ -302,48 +587,44 @@ class _ChatInfoScreen extends ConsumerState ), SliverToBoxAdapter( child: Container( - color: Palette.secondary, - child: Column( - children: [ - TabBarWidget( - controller: _tabController, - tabs: const [ - Tab(text: 'Media'), - Tab(text: 'Voice'), - ], - - ), - Flexible( - fit: FlexFit.loose, - child: TabBarView( - controller: _tabController, - children: List.generate(2, (index) { - return const Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - LottieViewer( - path: 'assets/tgs/EasterDuck.tgs', - width: 100, - height: 100, - ), - TitleElement( - name: 'To be implemented', - color: Palette.primaryText, - fontSize: Sizes.primaryText - 2, - fontWeight: FontWeight.bold, - padding: EdgeInsets.only(bottom: 0, top: 10), - ), - ], - ); - } - ), + color: Palette.secondary, + child: Column(children: [ + TabBarWidget( + controller: _tabController, + tabs: const [ + Tab(text: 'Media'), + Tab(text: 'Voice'), + ], + ), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: TabBarView( + controller: _tabController, + children: List.generate(2, (index) { + return const Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LottieViewer( + path: 'assets/tgs/EasterDuck.tgs', + width: 100, + height: 100, ), - ) - ] + TitleElement( + name: 'To be implemented', + color: Palette.primaryText, + fontSize: Sizes.primaryText - 2, + fontWeight: FontWeight.bold, + padding: EdgeInsets.only(bottom: 0, top: 10), + ), + ], + ); + }), ), ) - ), + ]), + )), ], ), ); diff --git a/lib/features/chat/view/screens/chat_screen.dart b/lib/features/chat/view/screens/chat_screen.dart index 01fdc14b..be6c1a2b 100644 --- a/lib/features/chat/view/screens/chat_screen.dart +++ b/lib/features/chat/view/screens/chat_screen.dart @@ -1,38 +1,42 @@ +// ignore_for_file: must_be_immutable + import 'dart:async'; -import 'dart:math'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_cupertino_datetime_picker/flutter_cupertino_datetime_picker.dart'; + import 'package:telware_cross_platform/core/constants/keys.dart'; import 'package:telware_cross_platform/core/mock/constants_mock.dart'; import 'package:telware_cross_platform/core/models/chat_model.dart'; import 'package:telware_cross_platform/core/models/message_model.dart'; -import 'package:telware_cross_platform/core/models/user_model.dart'; import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; -import 'package:telware_cross_platform/core/view/widget/lottie_viewer.dart'; +import 'package:telware_cross_platform/core/utils.dart'; +import 'package:telware_cross_platform/core/view/widget/popup_menu_widget.dart'; import 'package:telware_cross_platform/features/chat/classes/message_content.dart'; import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/chat/providers/chat_provider.dart'; -import 'package:telware_cross_platform/core/utils.dart'; -import 'package:telware_cross_platform/core/view/widget/popup_menu_item_widget.dart'; import 'package:telware_cross_platform/features/chat/services/audio_recording_service.dart'; import 'package:telware_cross_platform/features/chat/utils/chat_utils.dart'; import 'package:telware_cross_platform/features/chat/view/widget/bottom_input_bar_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/call_overlay_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/chat_header_widget.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/chat_messages_list.dart'; import 'package:telware_cross_platform/features/chat/view/widget/date_label_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/magic_recording_button.dart'; -import 'package:telware_cross_platform/features/chat/view/widget/message_tile_widget.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/new_chat_screen_sticker.dart'; import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_option_widget.dart'; +import 'package:telware_cross_platform/core/models/user_model.dart'; + import '../../../../core/routes/routes.dart'; import '../widget/reply_widget.dart'; import 'create_chat_screen.dart'; @@ -59,18 +63,21 @@ class _ChatScreen extends ConsumerState final ScrollController _scrollController = ScrollController(); late String _chosenAnimation; MessageModel? replyMessage; + MessageModel? editMessage; List selectedMessages = []; List pinnedMessages = []; int indexInPinnedMessage = 0; - ChatModel? chatModel; + late ChatModel chatModel; bool isSearching = false; bool isShowAsList = false; int _numberOfMatches = 0; - late bool _isMuted = false; + bool _isMuted = false; bool isLoading = true; bool isTextEmpty = true; bool showMuteOptions = false; + bool isAllowedToSend = true; + // ignore: prefer_final_fields int _currentMatch = 1; @@ -81,85 +88,75 @@ class _ChatScreen extends ConsumerState List _messageIndices = []; late Timer _draftTimer; - String _previousDraft = ""; + // String _previousDraft = ""; late ChatType type; @override void initState() { super.initState(); - chatModel = widget.chatModel; _messageController.text = widget.chatModel?.draft ?? ""; + WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { - chatModel = widget.chatModel ?? ref.watch(chatProvider(widget.chatId)); - setState(() { - _isMuted = chatModel!.isMuted; - _messageController.text = chatModel!.draft ?? ""; - }); - final messages = chatModel?.messages ?? []; - _updateChatMessages(messages); - pinnedMessages = messages.where((message) => message.isPinned).toList(); if (widget.forwardedMessages != null) { _sendForwardedMessages(); } }); _chosenAnimation = getRandomLottieAnimation(); - // _scrollToBottom(); - // Initialize the AudioRecorderService _audioRecorderService = AudioRecorderService(updateUI: setState); _draftTimer = Timer.periodic(const Duration(seconds: 3), (timer) { _updateDraft(); }); + } @override void dispose() { _messageController.dispose(); - _scrollController.dispose(); // Dispose of the ScrollController + _scrollController.dispose(); _audioRecorderService.dispose(); _draftTimer.cancel(); + SystemChannels.textInput.invokeMethod('TextInput.hide'); WidgetsBinding.instance.removeObserver(this); // Remove the observer super.dispose(); } void _updateDraft() async { // TODO : server return 500 status code every time try to fix it ASAP - return; - if (!mounted) return; - final currentDraft = _messageController.text; - if (currentDraft != _previousDraft) { - ref - .read(chattingControllerProvider) - .updateDraft(chatModel!, currentDraft); - _previousDraft = currentDraft; - } else if (chatModel?.id != null) { - // ref - // .read(chattingControllerProvider) - // .getDraft(chatModel!.id!) - // .then((draft) { - // if (draft != null && draft != _previousDraft) { - // setState(() { - // _messageController.text = draft; - // _previousDraft = draft; - // }); - // } - // }); - } + // if (!mounted) return; + // final currentDraft = _messageController.text; + // if (currentDraft != _previousDraft) { + // ref + // .read(chattingControllerProvider) + // .updateDraft(chatModel!, currentDraft); + // _previousDraft = currentDraft; + // } else if (chatModel?.id != null) { + // // ref + // // .read(chattingControllerProvider) + // // .getDraft(chatModel!.id!) + // // .then((draft) { + // // if (draft != null && draft != _previousDraft) { + // // setState(() { + // // _messageController.text = draft; + // // _previousDraft = draft; + // // }); + // // } + // // }); + // } + // return; } void _updateChatMessages(List messages) async { - _generateChatContentWithDateLabels(messages).then((content) { - setState(() { - chatContent = content; - }); + setState(() { + chatContent = _generateChatContentWithDateLabels(messages); }); } - Future> _generateChatContentWithDateLabels( - List messages) async { + List _generateChatContentWithDateLabels( + List messages) { List chatContent = []; for (int i = 0; i < messages.length; i++) { if (i == 0 || @@ -237,7 +234,7 @@ class _ChatScreen extends ConsumerState } //TODO: Implement the sendMsg method with another types of messages - void _sendMessage({ +void _sendMessage({ required WidgetRef ref, required String contentType, String? fileName, @@ -269,7 +266,7 @@ class _ChatScreen extends ConsumerState return; } } - + if (mediaUrl != null || (!UPLOAD_MEDIA && needUploadMedia)) { messageText = caption ?? ''; if (isMusic == false && contentType == 'audio') { @@ -281,14 +278,14 @@ class _ChatScreen extends ConsumerState fileName = '$displayName ➜ ${chatModel?.title}'; } } - + content = createMessageContent( - contentType: messageContentType, - filePath: filePath, - fileName: fileName, - mediaUrl: mediaUrl, - isMusic: isMusic, - text: messageText, + contentType: messageContentType, + filePath: filePath, + fileName: fileName, + mediaUrl: mediaUrl, + isMusic: isMusic, + text: messageText, ); // TODO : Handle media attribute in the request of sending a message @@ -302,28 +299,29 @@ class _ChatScreen extends ConsumerState parentMessage: replyMessage?.id, ); _messageController.clear(); - _updateChatMessages([...?chatModel?.messages, newMessage]); - ref - .read(chattingControllerProvider) - .sendMsg( - content: newMessage.content!, - msgType: newMessage.messageType, - contentType: newMessage.messageContentType, - chatType: ChatType.private, - chatModel: chatModel, - ) - .then((_) { - List messages = - ref.read(chatProvider(chatModel!.id!))?.messages ?? []; - _updateChatMessages(messages); - debugPrint(" ${messages.toString()}"); - // _scrollToBottom(); - }); + ref.read(chattingControllerProvider).sendMsg( + content: newMessage.content!, + msgType: newMessage.messageType, + contentType: newMessage.messageContentType, + chatType: ChatType.private, + chatModel: chatModel, + parentMessgeId: replyMessage?.id); + } + + void _editMessage() { + if (editMessage == null || _messageController.text.isEmpty) return; + // todo(ahmed): handle extreem cases like editing a message that is not yet sent + ref.read(chattingControllerProvider).editMsg( + (editMessage?.id)!, + (chatModel.id)!, + _messageController.text, + ); + _messageController.text = ''; } void _setChatMute(bool mute, DateTime? muteUntil) async { if (!mute) { - ref.read(chattingControllerProvider).unmuteChat(chatModel!).then((_) { + ref.read(chattingControllerProvider).unmuteChat(chatModel).then((_) { debugPrint('Unmuted until: $muteUntil, chat: $chatModel'); setState(() { _isMuted = false; @@ -332,7 +330,7 @@ class _ChatScreen extends ConsumerState } else { ref .read(chattingControllerProvider) - .muteChat(chatModel!, muteUntil) + .muteChat(chatModel, muteUntil) .then((_) { debugPrint('Muted until: $muteUntil, chat: $chatModel'); setState(() { @@ -342,40 +340,13 @@ class _ChatScreen extends ConsumerState } } - void _removeReply() { + void _unreferenceMessages() { setState(() { replyMessage = null; + editMessage = null; }); } - //---------------------------------------------------------------------------- - //-------------------------------Media---------------------------------------- - - void onMediaDownloaded( - String? filePath, String? messageLocalId, String? chatId) { - if (filePath == null) { - showToastMessage('File has been deleted ask the sender to resend it'); - return; - } - if (messageLocalId == null) { - showToastMessage('File does not exist please upload it again'); - return; - } - if (chatId == null) { - showToastMessage('Chat ID is missing'); - return; - } - debugPrint("Downloaded file path: $filePath"); - debugPrint("Message local ID: $messageLocalId"); - debugPrint("Chat ID: $chatId"); - ref - .read(chattingControllerProvider) - .editMessageFilePath(chatId, messageLocalId, filePath); - List messages = - ref.watch(chatProvider(chatModel!.id!))?.messages ?? []; - _updateChatMessages(messages); - } - void _searchForText(searchText) async { Map>> messageMatches = {}; int numberOfMatches = 0; @@ -406,7 +377,6 @@ class _ChatScreen extends ConsumerState } void _enableSearching() { - // Implement search functionality here setState(() { isSearching = true; }); @@ -445,53 +415,72 @@ class _ChatScreen extends ConsumerState @override Widget build(BuildContext context) { debugPrint('&*&**&**& rebuild chat screen'); - // ref.watch(chatsViewModelProvider); - final popupMenu = buildPopupMenu(); - final chatModelImage = chatModel ?? ref.watch(chatProvider(widget.chatId))!; + final chats = ref.watch(chatsViewModelProvider); + final index = chats.indexWhere((chat) => chat.id == widget.chatId); + final ChatModel? chat = + (index == -1) ? null : chats[index]; // Chat not found + chatModel = widget.chatModel ?? chat!; _updateDraft(); - final type = chatModelImage.type; - final String title = chatModelImage.title; - final membersNumber = chatModelImage.userIds.length; - final String subtitle = chatModelImage.type == ChatType.private + final type = chatModel.type; + final String title = chatModel.title; + final membersNumber = chatModel.userIds.length; + final imageBytes = chatModel.photoBytes; + final photo = chatModel.photo; + final messages = chatModel.messages; + final chatID = chatModel.id; + final String subtitle = chatModel.type == ChatType.private ? "last seen a long time ago" : "$membersNumber Member${membersNumber > 1 ? "s" : ""}"; - final imageBytes = chatModelImage.photoBytes; - final photo = chatModelImage.photo; - final chatID = chatModelImage.id; - var messagesIndex = 0; - return Scaffold( - appBar: selectedMessages.isEmpty - ? AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - if (isShowAsList) { - setState(() { - isShowAsList = false; - }); - } else if (isSearching) { - setState(() { - isSearching = false; - _messageMatches.clear(); - }); - } else { - _updateDraft(); - Navigator.pop(context); - } - }, - ), - title: !isSearching - ? GestureDetector( - onTap: () { - if (chatModel?.type == ChatType.private) { + _isMuted = chatModel.isMuted; + if (chatModel.draft != null && chatModel.draft!.isNotEmpty) { + _messageController.text = chatModel.draft ?? ''; + } + chatContent = _generateChatContentWithDateLabels(messages); + pinnedMessages = messages.where((message) => message.isPinned).toList(); + // debugPrint('pinned Messages count after is : ${pinnedMessages.length}'); + + if(chatModel.messagingPermission == false){ + setState(() { + isAllowedToSend = chatModel.admins!.contains(ref.read(userProvider)?.id) ; + }); + } + return GestureDetector( + onTap: () { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + }, + child: Scaffold( + appBar: selectedMessages.isEmpty + ? AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (isShowAsList) { + setState(() { + isShowAsList = false; + }); + } else if (isSearching) { + setState(() { + isSearching = false; + _messageMatches.clear(); + }); + } else { + _updateDraft(); + context.push(Routes.home); + } + }, + ), + title: !isSearching + ? GestureDetector( + onTap: () { + if (chatModel.type == ChatType.private) { context.push(Routes.userProfile, - extra: chatModel!.userIds.firstWhere( + extra: chatModel.userIds.firstWhere( (element) => element != ref.read(userProvider)!.id)); } else { context.push(Routes.chatInfoScreen, - extra: chatModel!); + extra: chatModel); } }, child: ChatHeaderWidget( @@ -501,584 +490,481 @@ class _ChatScreen extends ConsumerState imageBytes: imageBytes, ), ) - : TextField( - key: ChatKeys.chatSearchInput, - autofocus: true, - decoration: const InputDecoration( - hintText: 'Search', - hintStyle: TextStyle( - color: Palette.accentText, - fontWeight: FontWeight.w400), - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, + : TextField( + key: ChatKeys.chatSearchInput, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Search', + hintStyle: TextStyle( + color: Palette.accentText, + fontWeight: FontWeight.w400), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + onSubmitted: _searchForText, + onChanged: (value) => { + if (isShowAsList) + { + setState(() { + isShowAsList = false; + }) + } + }, ), - onSubmitted: _searchForText, - onChanged: (value) => { - if (isShowAsList) - { - setState(() { - isShowAsList = false; - }) - } - }, - ), - actions: [ - if (!isSearching) - PopupMenuButton( + actions: [ + if (!isSearching) + IconButton( icon: const Icon(Icons.more_vert), - onSelected: _handlePopupMenuSelection, - color: Palette.secondary, - padding: EdgeInsets.zero, - itemBuilder: popupMenu, + onPressed: _showMoreSettings, ), ], ) - : AppBar( - backgroundColor: Palette.secondary, - leading: GestureDetector( - onTap: () { - setState(() { - selectedMessages = []; - }); - }, - child: const Icon(Icons.close), - ), - title: Row( - children: [ - // Number - Text( - selectedMessages.length.toString(), - style: const TextStyle(color: Colors.white, fontSize: 18), - ), - const Spacer(), - // Copy icon - IconButton( - icon: const Icon(Icons.copy, color: Colors.white), - onPressed: () {}, - ), - // Share icon - IconButton( - icon: const Icon(FontAwesomeIcons.share, - color: Colors.white), - onPressed: () { - context.push(CreateChatScreen.route); - }, - ), - // Delete icon - IconButton( - icon: const Icon(Icons.delete, color: Colors.white), - onPressed: () { - // TODO call delete function - }, - ), - ], + : AppBar( + backgroundColor: Palette.secondary, + leading: GestureDetector( + onTap: () { + setState(() { + selectedMessages = []; + }); + }, + child: const Icon(Icons.close), + ), + title: Row( + children: [ + // Number + Text( + selectedMessages.length.toString(), + style: + const TextStyle(color: Colors.white, fontSize: 18), + ), + const Spacer(), + // Copy icon + IconButton( + icon: const Icon(Icons.copy, color: Colors.white), + onPressed: () {}, + ), + // Share icon + IconButton( + icon: const Icon(FontAwesomeIcons.share, + color: Colors.white), + onPressed: () { + context.push(CreateChatScreen.route); + + }, + ), + // Delete icon + IconButton( + icon: const Icon(Icons.delete, color: Colors.white), + onPressed: () { + // TODO call delete function + }, + ), + ], + ), ), - ), - body: Column( + body: Column( children: [ const CallOverlay(), Expanded( - child: Stack( + child:Stack( + children: [ + // Chat content area (with background SVG) + Positioned.fill( + child: SvgPicture.asset( + 'assets/svg/default_pattern.svg', + fit: BoxFit.cover, + colorFilter: const ColorFilter.mode( + Palette.trinary, + BlendMode.srcIn, + ), + ), + ), + Column( children: [ - // Chat content area (with background SVG) - Positioned.fill( - child: SvgPicture.asset( - 'assets/svg/default_pattern.svg', - fit: BoxFit.cover, - colorFilter: const ColorFilter.mode( - Palette.trinary, - BlendMode.srcIn, - ), - ), + Expanded( + child: isShowAsList + ? Container( + color: Palette.background, + child: Column( + children: _messageIndices.map((index) { + MessageModel msg = chatContent[index]; + return SettingsOptionWidget( + imagePath: getRandomImagePath(), + text: msg.senderId, + subtext: msg.content?.toJson()['text'] ?? "", + onTap: () => { + // TODO (Mo): Create scroll to msg + }, + ); + }).toList(), + ), + ) + : chatContent.isEmpty + ? NewChatScreenSticker( + chosenAnimation: _chosenAnimation) + : ChatMessagesList( + messages: messages, + scrollController: _scrollController, + chatContent: chatContent, + selectedMessages: selectedMessages, + type: type, + messageMatches: _messageMatches, + replyMessage: replyMessage, + chatId: chatID, + pinnedMessages: pinnedMessages, + updateChatMessages: + _generateChatContentWithDateLabels, + onPin: _onPin, + onLongPress: _onLongPress, + onReply: _onReply, + onEdit: _onEdit), ), - Column( - children: [ - Expanded( - child: isShowAsList - ? Container( - color: Palette.background, - child: Column( - children: _messageIndices.map((index) { - MessageModel msg = chatContent[index]; - return SettingsOptionWidget( - imagePath: getRandomImagePath(), - text: msg.senderId, - subtext: - msg.content?.toJson()['text'] ?? "", - onTap: () => { - // TODO (Mo): Create scroll to msg - }, - ); - }).toList(), - ), - ) - : chatContent.isEmpty - ? Center( - child: Container( - width: 210, - margin: const EdgeInsets.symmetric( - horizontal: 24.0), - padding: const EdgeInsets.all(22.0), - decoration: BoxDecoration( - color: const Color.fromRGBO( - 4, 86, 57, 0.30), - borderRadius: - BorderRadius.circular(16.0), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4.0, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - const Text( - 'No messages here yet...', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Palette.primaryText, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - const Text( - 'Send a message or tap the greeting below.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Palette.primaryText, - ), - ), - const SizedBox(height: 20), - LottieViewer( - path: _chosenAnimation, - width: 100, - height: 100, - isLooping: true, - ), - ], - ), - ), - ) - : SingleChildScrollView( - reverse: true, - controller: - _scrollController, // Use the ScrollController - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 10), - child: Column( - children: chatContent - .mapIndexed((index, item) { - if (item is DateLabelWidget) { - return item; - } else if (item is MessageModel) { - return Row( - mainAxisAlignment: item - .senderId == - ref.read(userProvider)!.id - ? selectedMessages.isNotEmpty - ? MainAxisAlignment - .spaceBetween - : MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - if (selectedMessages - .isNotEmpty) // Show check icon only if selected - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: - 1), // White border - ), - child: CircleAvatar( - radius: 12, - backgroundColor: - selectedMessages - .contains( - item) == - true - ? Colors.green - : Colors - .transparent, - child: selectedMessages - .contains( - item) == - true - ? const Icon( - Icons.check, - color: - Colors.white, - size: 16) - : const SizedBox(), - ), - ), - if (selectedMessages - .contains(item)) - const SizedBox(width: 10), - MessageTileWidget( - key: ValueKey( - '${MessageKeys.messagePrefix}${messagesIndex++}'), - messageModel: item, - isSentByMe: item.senderId == - ref - .read(userProvider)! - .id, - showInfo: - type == ChatType.group, - highlights: - _messageMatches[index] ?? - const [ - MapEntry(0, 0) - ], - onDownloadTap: - (String? filePath) { - onMediaDownloaded(filePath, - item.localId, chatID); - }, - onReply: (message) { - setState(() { - replyMessage = message; - }); - }, - onPin: (message) { - setState(() { - pinnedMessages - .contains(message) - ? pinnedMessages - .remove(message) - : pinnedMessages - .add(message); - }); - }, - onPress: - selectedMessages.isEmpty - ? null - : () {}, - onLongPress: (message) { - setState(() { - replyMessage = null; - selectedMessages - .contains(message) - ? selectedMessages - .remove(message) - : selectedMessages - .add(message); - }); - }, - onDelete: (msg, _, id) {}, - ), - ], - ); - } else { - return const SizedBox.shrink(); - } - }).toList(), - ), - ), + if (replyMessage != null) + ReplyEditFieldHeader( + message: replyMessage!, + isReplyOrEdit: true, + onDiscard: () { + setState(() { + replyMessage = null; + editMessage = null; + }); + }, + ), + if (editMessage != null) + ReplyEditFieldHeader( + message: editMessage!, + isReplyOrEdit: false, + onDiscard: () { + setState(() { + replyMessage = null; + editMessage = null; + _messageController.text = ''; + }); + }, + ), + if (selectedMessages.isNotEmpty) + Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + setState(() { + replyMessage = selectedMessages[0]; + selectedMessages = []; + }); + }, + child: Row( + children: [ + selectedMessages.length == 1 + ? const Icon( + Icons.reply, + ) + : const SizedBox(), + const SizedBox( + width: 5, ), + selectedMessages.length == 1 + ? const Text( + 'Reply', + style: TextStyle(color: Colors.white), + ) + : const SizedBox(), + ], + ), + ), + GestureDetector( + onTap: () { + context.push(CreateChatScreen.route); + }, + child: const Row( + children: [ + Icon(FontAwesomeIcons.share), + SizedBox( + width: 5, + ), + Text( + 'Forward', + style: TextStyle(color: Colors.white), + ), + ], + ), + ) + ], + ), ), - if (replyMessage != null) - ReplyWidget( - message: replyMessage!, - onDiscard: () { - setState(() { - replyMessage = null; - }); - }, - ) - else - const SizedBox(), - if (selectedMessages.isNotEmpty) - Container( - color: Palette.secondary, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: () { - setState(() { - replyMessage = selectedMessages[0]; - selectedMessages = []; - }); - }, - child: Row( - children: [ - selectedMessages.length == 1 - ? const Icon( - Icons.reply, - ) - : const SizedBox(), - const SizedBox( - width: 5, - ), - selectedMessages.length == 1 - ? const Text( - 'Reply', - style: TextStyle( - color: Colors.white), - ) - : const SizedBox(), - ], + ) + else if (!isSearching) + BottomInputBarWidget( + isEditing: editMessage != null, + controller: _messageController, + audioRecorderService: _audioRecorderService, + chatID: chatID, + sendMessage: _sendMessage, + unreferenceMessages: _unreferenceMessages, + editMessage: _editMessage, + ) + else + Container( + color: Palette.trinary, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + key: ChatKeys.chatSearchDatePicker, + icon: const Icon(Icons.edit_calendar), + onPressed: () { + // Show the Cupertino Date Picker when the icon is pressed + DatePicker.showDatePicker( + context, + pickerTheme: const DateTimePickerTheme( + backgroundColor: Palette.secondary, + itemTextStyle: TextStyle( + color: Palette.primaryText, + fontSize: 20, + fontWeight: FontWeight.w600, ), - ), - GestureDetector( - onTap: () { - context.push(CreateChatScreen.route); - }, - child: const Row( - children: [ - Icon(FontAwesomeIcons.share), - SizedBox( - width: 5, - ), - Text( - 'Forward', - style: TextStyle(color: Colors.white), - ), - ], + confirm: Text( + 'Jump to date', + style: TextStyle( + color: Palette.primary, + fontSize: 18, + fontWeight: FontWeight.w500, + ), ), - ) - ], - ), + cancel: null, + ), + minDateTime: DateTime.now() + .subtract(const Duration(days: 365 * 10)), + maxDateTime: DateTime.now(), + initialDateTime: DateTime.now(), + dateFormat: 'dd-MMMM-yyyy', + locale: DateTimePickerLocale.en_us, + onConfirm: (date, time) { + _scrollToTimeStamp(date); + }, + ); + }, ), - ) - else if (!isSearching) - BottomInputBarWidget( - controller: _messageController, - audioRecorderService: _audioRecorderService, - chatID: chatID, - sendMessage: _sendMessage, - removeReply: _removeReply, - ) - else - Container( - color: Palette.trinary, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - key: ChatKeys.chatSearchDatePicker, - icon: const Icon(Icons.edit_calendar), - onPressed: () { - // Show the Cupertino Date Picker when the icon is pressed - DatePicker.showDatePicker( - context, - pickerTheme: const DateTimePickerTheme( - backgroundColor: Palette.secondary, - itemTextStyle: TextStyle( - color: Palette.primaryText, - fontSize: 20, - fontWeight: FontWeight.w600, - ), - confirm: Text( - 'Jump to date', - style: TextStyle( - color: Palette.primary, - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - cancel: null, - ), - minDateTime: DateTime.now().subtract( - const Duration(days: 365 * 10)), - maxDateTime: DateTime.now(), - initialDateTime: DateTime.now(), - dateFormat: 'dd-MMMM-yyyy', - locale: DateTimePickerLocale.en_us, - onConfirm: (date, time) { - _scrollToTimeStamp(date); - }, - ); + if (_numberOfMatches != 0) + Text( + _numberOfMatches == 0 + ? 'No results' + : isShowAsList + ? '$_numberOfMatches result${_numberOfMatches != 1 ? 's' : ''}' + : '$_currentMatch of $_numberOfMatches', + style: const TextStyle( + color: Palette.primaryText, + fontWeight: FontWeight.w500), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () { + _toggleSearchDisplay(); }, - ), - if (_numberOfMatches != 0) - Text( - _numberOfMatches == 0 - ? 'No results' - : isShowAsList - ? '$_numberOfMatches result${_numberOfMatches != 1 ? 's' : ''}' - : '$_currentMatch of $_numberOfMatches', + child: Text( + key: ChatKeys.chatSearchShowMode, + isShowAsList + ? 'Show as Chat' + : 'Show as List', style: const TextStyle( - color: Palette.primaryText, - fontWeight: FontWeight.w500), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: GestureDetector( - onTap: () { - _toggleSearchDisplay(); - }, - child: Text( - key: ChatKeys.chatSearchShowMode, - isShowAsList - ? 'Show as Chat' - : 'Show as List', - style: const TextStyle( - color: Palette.accent, - fontSize: 16, - ), - ), + color: Palette.accent, + fontSize: 16, ), ), ), - ], + ), ), - ), - ], - ), - pinnedMessages.isNotEmpty - ? Positioned( - top: 0, - // Adjust this to position the widget from the top of the screen - left: 0, - right: 0, - child: Container( - color: Palette - .secondary, // Example background color for the widget - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ], + ), + ), + ], + ), + pinnedMessages.isNotEmpty + ? Positioned( + top: 0, + // Adjust this to position the widget from the top of the screen + left: 0, + right: 0, + child: Container( + color: Palette + .secondary, // Example background color for the widget + padding: const EdgeInsets.symmetric( + vertical: 10, horizontal: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( children: [ - Row( + Column( + children: List.generate(pinnedMessages.length, + (index) { + return Container( + height: 40 / pinnedMessages.length, + padding: const EdgeInsets.all(1.0), + margin: const EdgeInsets.all(1.0), + decoration: BoxDecoration( + color: Colors.blueAccent, + borderRadius: + BorderRadius.circular(8.0), + ), + ); + }), + ), + const SizedBox( + width: 8, + ), + const Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Column( - children: List.generate( - pinnedMessages.length, (index) { - return Container( - height: 40 / pinnedMessages.length, - padding: const EdgeInsets.all(1.0), - margin: const EdgeInsets.all(1.0), - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: - BorderRadius.circular(8.0), - ), - ); - }), - ), - const SizedBox( - width: 8, - ), - const Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Pinned Message', - style: TextStyle( - color: Palette.primary, - fontSize: 12), - ), - Text( - // pinnedMessages[indexInPinnedMessage].content as String, - 'Content placeholder', - style: TextStyle(fontSize: 12), - ) - ], + Text( + 'Pinned Message', + style: TextStyle( + color: Palette.primary, fontSize: 12), ), + Text( + // pinnedMessages[indexInPinnedMessage].content as String, + 'Content placeholder', + style: TextStyle(fontSize: 12), + ) ], ), - GestureDetector( - onTap: () { - List senderIds = []; - for (var message in pinnedMessages) { - senderIds.add(message.senderId); - } - ChatModel newChat = ChatModel( - title: 'pinnedMessages', - userIds: senderIds, - type: ChatType.group, - messages: pinnedMessages); - context.push(Routes.pinnedMessagesScreen, - extra: newChat); - }, - child: const Icon( - Icons.menu_open_outlined, - color: Palette.accentText, - ), - ) ], ), - ), - ) - : const SizedBox(), - if (isSearching && _numberOfMatches != 0) ...[ - Positioned( - bottom: 150, - right: 10, - child: GestureDetector( - onTap: _scrollToPrevMatch, - child: Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: Palette.quaternary, - borderRadius: BorderRadius.circular(50), - ), - child: const Center( - child: Icon(Icons.keyboard_arrow_up_sharp), - )), - ), - ), - Positioned( - bottom: 90, - right: 10, - child: GestureDetector( - onTap: _scrollToNextMatch, - child: Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: Palette.quaternary, - borderRadius: BorderRadius.circular(50), - ), - child: const Center( - child: Icon(Icons.keyboard_arrow_down_sharp), - )), + GestureDetector( + onTap: () { + List senderIds = []; + for (var message in pinnedMessages) { + senderIds.add(message.senderId); + } + ChatModel newChat = ChatModel( + title: 'pinnedMessages', + userIds: senderIds, + type: ChatType.group, + messages: pinnedMessages); + context.push(Routes.pinnedMessagesScreen, + extra: newChat); + }, + child: const Icon( + Icons.menu_open_outlined, + color: Palette.accentText, + ), + ) + ], + ), ), - ), - ], - MagicRecordingButton( - audioRecorderService: _audioRecorderService, - sendMessage: ( - {required String contentType, String? filePath}) { - _sendMessage( - ref: ref, - contentType: contentType, - filePath: filePath); - }) - ], + ) + : const SizedBox(), + if (isSearching && _numberOfMatches != 0) ...[ + Positioned( + bottom: 150, + right: 10, + child: GestureDetector( + onTap: _scrollToPrevMatch, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Palette.quaternary, + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Icon(Icons.keyboard_arrow_up_sharp), + )), + ), + ), + Positioned( + bottom: 90, + right: 10, + child: GestureDetector( + onTap: _scrollToNextMatch, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: Palette.quaternary, + borderRadius: BorderRadius.circular(50), + ), + child: const Center( + child: Icon(Icons.keyboard_arrow_down_sharp), + )), + ), + ), + ], + MagicRecordingButton( + audioRecorderService: _audioRecorderService, + sendMessage: ( + {required String contentType, String? filePath}) { + _sendMessage( + ref: ref, contentType: contentType, filePath: filePath); + }), + ], ), ) ], - )); + ) + ),); + } + + void _showMoreSettings() { + var items = []; + if (showMuteOptions) { + items = [ + {'icon': Icons.arrow_back, 'text': 'Back', 'value': 'no-close'}, + {'icon': Icons.music_off_outlined, 'text': 'Disable sound', 'value': 'disable-sound'}, + {'icon': Icons.access_time_rounded, 'text': 'Mute for 30m', 'value': 'mute-30m'}, + {'icon': Icons.notifications_paused_outlined, 'text': 'Mute for...', 'value': 'mute-custom'}, + {'icon': Icons.tune_outlined, 'text': 'Customize', 'value': 'customize'}, + {'icon': Icons.volume_off_outlined, 'text': 'Mute Forever', 'value': 'mute-forever', 'color': Palette.error}, + ]; + } else { + if (_isMuted) { + items = [ + {'icon': Icons.volume_off_outlined, 'text': 'Unmute', 'value': 'unmute-chat'}, + ]; + } else { + items = [ + {'icon': Icons.volume_up_outlined, 'text': 'Mute', 'value': 'no-close', + 'trailing': const Icon(Icons.arrow_forward_ios_rounded, + color: Palette.inactiveSwitch, size: 16) + }, + ]; + } + items.addAll([ + {'icon': Icons.videocam_outlined, 'text': 'Video Call', 'value': 'video-call'}, + {'icon': Icons.search, 'text': 'Search', 'value': 'search'}, + {'icon': Icons.wallpaper_rounded, 'text': 'Change Wallpaper', 'value': 'change-wallpaper'}, + {'icon': Icons.cleaning_services, 'text': 'Clear History', 'value': 'clear-history'}, + {'icon': Icons.delete_outline, 'text': 'Delete Chat', 'value': 'delete-chat'}, + ]); + } + + final renderBox = context.findRenderObject() as RenderBox; + final position = Offset(renderBox.size.width, -350); + + PopupMenuWidget.showPopupMenu( + context: context, + position: position, + items: items, + onSelected: _handlePopupMenuSelection + ); } void _handlePopupMenuSelection(String value) { - final bool noChat = chatModel?.id == null; + final bool noChat = chatModel.id == null; switch (value) { - // case 'no-close': - // break; + case 'no-close': + showMuteOptions = !showMuteOptions; + break; case 'search': if (noChat) { showToastMessage("There is nothing to search"); @@ -1086,7 +972,7 @@ class _ChatScreen extends ConsumerState } _enableSearching(); break; - case 'mute-chat': + case 'mute-custom': showMuteOptions = false; if (noChat) { showToastMessage("Maybe say something first..."); @@ -1124,7 +1010,15 @@ class _ChatScreen extends ConsumerState case 'unmute-chat': _setChatMute(false, null); break; - case 'mute-chat-forever': + case 'mute-30m': + if (noChat) { + showToastMessage("You can't mute nothing"); + return; + } + showMuteOptions = false; + _setChatMute(true, DateTime.now().add(const Duration(minutes: 30))); + break; + case 'mute-forever': if (noChat) { showToastMessage("Seriously? Mute what?"); return; @@ -1136,198 +1030,44 @@ class _ChatScreen extends ConsumerState } } - PopupMenuItemBuilder buildPopupMenu() { - const double menuItemsHeight = 45.0; - if (!mounted) return (BuildContext context) => []; - return (BuildContext context) { - if (showMuteOptions) { - return [ - PopupMenuItem( - value: 'no-close', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: GestureDetector( - onTap: () { - setState(() { - showMuteOptions = false; - }); - }, - child: const PopupMenuItemWidget( - icon: Icons.arrow_back, - text: 'Back', - ), - )), - const PopupMenuItem( - value: 'disable-sound', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.music_off_outlined, - text: 'Disable sound', - ), - ), - const PopupMenuItem( - key: ChatKeys.chatSearchButton, - value: 'mute-chat', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.notifications_paused_outlined, - text: 'Mute for...', - ), - ), - const PopupMenuItem( - value: 'customize-chat', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.tune_outlined, - text: 'Customize', - ), - ), - const PopupMenuItem( - value: 'mute-chat-forever', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.volume_off_outlined, - text: 'Mute Forever', - ), - ), - ]; - } - return [ - if (_isMuted) - const PopupMenuItem( - value: 'unmute-chat', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.volume_off_outlined, - text: 'Unmute', - )) - else - PopupMenuItem( - value: 'no-close', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: GestureDetector( - onTap: () { - setState(() { - showMuteOptions = true; - }); - }, - child: const PopupMenuItemWidget( - icon: Icons.volume_up_rounded, - text: 'Mute', - trailing: Icon( - Icons.arrow_forward_ios_rounded, - color: Palette.inactiveSwitch, - size: 16, - )), - ), - ), - const PopupMenuDivider( - height: 10, - ), - const PopupMenuItem( - value: 'video-call', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.videocam_outlined, - text: 'Video Call', - ), - ), - const PopupMenuItem( - key: ChatKeys.chatSearchButton, - value: 'search', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.search, - text: 'Search', - ), - ), - const PopupMenuItem( - value: 'change-wallpaper', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.wallpaper_rounded, - text: 'Change Wallpaper', - ), - ), - const PopupMenuItem( - value: 'clear-history', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.cleaning_services, - text: 'Clear History', - ), - ), - const PopupMenuItem( - value: 'delete-chat', - padding: EdgeInsets.zero, - height: menuItemsHeight, - child: PopupMenuItemWidget( - icon: Icons.delete_outline, - text: 'Delete Chat', - ), - ), - ]; - }; + void _onPin(MessageModel message) { + setState(() { + pinnedMessages.contains(message) + ? pinnedMessages.remove(message) + : pinnedMessages.add(message); + ref.read(chattingControllerProvider).pinMessageClient( + message.id ?? '', + chatModel.id ?? '', + ); + }); } - String getRandomLottieAnimation() { - // List of Lottie animation paths - List lottieAnimations = [ - 'assets/tgs/curious_pigeon.tgs', - 'assets/tgs/fruity_king.tgs', - 'assets/tgs/graceful_elmo.tgs', - 'assets/tgs/hello_anteater.tgs', - 'assets/tgs/hello_astronaut.tgs', - 'assets/tgs/hello_badger.tgs', - 'assets/tgs/hello_bee.tgs', - 'assets/tgs/hello_cat.tgs', - 'assets/tgs/hello_clouds.tgs', - 'assets/tgs/hello_duck.tgs', - 'assets/tgs/hello_elmo.tgs', - 'assets/tgs/hello_fish.tgs', - 'assets/tgs/hello_flower.tgs', - 'assets/tgs/hello_food.tgs', - 'assets/tgs/hello_fridge.tgs', - 'assets/tgs/hello_ghoul.tgs', - 'assets/tgs/hello_king.tgs', - 'assets/tgs/hello_lama.tgs', - 'assets/tgs/hello_monkey.tgs', - 'assets/tgs/hello_pigeon.tgs', - 'assets/tgs/hello_possum.tgs', - 'assets/tgs/hello_rat.tgs', - 'assets/tgs/hello_seal.tgs', - 'assets/tgs/hello_shawn_sheep.tgs', - 'assets/tgs/hello_snail_rabbit.tgs', - 'assets/tgs/hello_virus.tgs', - 'assets/tgs/hello_water_animal.tgs', - 'assets/tgs/hello_whales.tgs', - 'assets/tgs/muscles_wizard.tgs', - 'assets/tgs/plague_doctor.tgs', - 'assets/tgs/screaming_elmo.tgs', - 'assets/tgs/shy_elmo.tgs', - 'assets/tgs/sick_wizard.tgs', - 'assets/tgs/snowman.tgs', - 'assets/tgs/spinny_jelly.tgs', - 'assets/tgs/sus_moon.tgs', - 'assets/tgs/toiletpaper.tgs', - ]; + void _onLongPress(MessageModel message) { + setState(() { + replyMessage = null; + selectedMessages.contains(message) + ? selectedMessages.remove(message) + : selectedMessages.add(message); + }); + } - // Generate a random index - Random random = Random(); - int randomIndex = - random.nextInt(lottieAnimations.length); // Gets a random index + void _onReply(MessageModel message) { + setState(() { + debugPrint('reply'); + replyMessage = message; + editMessage = null; + }); + } - // Return the randomly chosen Lottie animation path - return lottieAnimations[randomIndex]; + void _onEdit(MessageModel message) { + setState(() { + debugPrint('edit'); + editMessage = message; + replyMessage = null; + debugPrint(message.content?.getContent()); + _messageController.text = + message.content?.getContent() ?? 'what about this'; + debugPrint('${_messageController.text} look here'); + }); } -} +} \ No newline at end of file diff --git a/lib/features/chat/view/screens/pinned_messages_screen.dart b/lib/features/chat/view/screens/pinned_messages_screen.dart index f5e9284e..8db91727 100644 --- a/lib/features/chat/view/screens/pinned_messages_screen.dart +++ b/lib/features/chat/view/screens/pinned_messages_screen.dart @@ -181,229 +181,224 @@ class _PinnedMessagesScreen extends ConsumerState final type = chatModel.type; var messagesIndex = 0; return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - if (isShowAsList) { - setState(() { - isShowAsList = false; - }); - } else if (isSearching) { - setState(() { - isSearching = false; - _messageMatches.clear(); - }); - } else { - Navigator.pop(context); - } - }, - ), - title: - Text('${chatModel.messages.length.toString()} Pinned Messages'), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + if (isShowAsList) { + setState(() { + isShowAsList = false; + }); + } else if (isSearching) { + setState(() { + isSearching = false; + _messageMatches.clear(); + }); + } else { + Navigator.pop(context); + } + }, ), - body: Stack( - children: [ - // Chat content area (with background SVG) - Positioned.fill( - child: SvgPicture.asset( - 'assets/svg/default_pattern.svg', - fit: BoxFit.cover, - colorFilter: const ColorFilter.mode( - Palette.trinary, - BlendMode.srcIn, - ), + title: Text('${chatModel.messages.length.toString()} Pinned Messages'), + ), + body: Stack( + children: [ + // Chat content area (with background SVG) + Positioned.fill( + child: SvgPicture.asset( + 'assets/svg/default_pattern.svg', + fit: BoxFit.cover, + colorFilter: const ColorFilter.mode( + Palette.trinary, + BlendMode.srcIn, ), ), - Column( - children: [ - Expanded( - child: isShowAsList - ? Container( - color: Palette.background, - child: Column( - children: _messageIndices.map((index) { - MessageModel msg = chatContent[index]; - return SettingsOptionWidget( - imagePath: getRandomImagePath(), - text: msg.senderId, - subtext: msg.content?.toJson()['text'] ?? "", - onTap: () => { - // TODO (Mo): Create scroll to msg - }, - ); - }).toList(), - ), - ) - : chatContent.isEmpty - ? Center( - child: Container( - width: 210, - margin: const EdgeInsets.symmetric( - horizontal: 24.0), - padding: const EdgeInsets.all(22.0), - decoration: BoxDecoration( - color: const Color.fromRGBO(4, 86, 57, 0.30), - borderRadius: BorderRadius.circular(16.0), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4.0, - offset: Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'No messages here yet...', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Palette.primaryText, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - const Text( - 'Send a message or tap the greeting below.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Palette.primaryText, - ), + ), + Column( + children: [ + Expanded( + child: isShowAsList + ? Container( + color: Palette.background, + child: Column( + children: _messageIndices.map((index) { + MessageModel msg = chatContent[index]; + return SettingsOptionWidget( + imagePath: getRandomImagePath(), + text: msg.senderId, + subtext: msg.content?.toJson()['text'] ?? "", + onTap: () => { + // TODO (Mo): Create scroll to msg + }, + ); + }).toList(), + ), + ) + : chatContent.isEmpty + ? Center( + child: Container( + width: 210, + margin: + const EdgeInsets.symmetric(horizontal: 24.0), + padding: const EdgeInsets.all(22.0), + decoration: BoxDecoration( + color: const Color.fromRGBO(4, 86, 57, 0.30), + borderRadius: BorderRadius.circular(16.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'No messages here yet...', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Palette.primaryText, ), - const SizedBox(height: 20), - LottieViewer( - path: _chosenAnimation, - width: 100, - height: 100, - isLooping: true, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Text( + 'Send a message or tap the greeting below.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Palette.primaryText, ), - ], - ), + ), + const SizedBox(height: 20), + LottieViewer( + path: _chosenAnimation, + width: 100, + height: 100, + isLooping: true, + ), + ], ), - ) - : SingleChildScrollView( - controller: - _scrollController, // Use the ScrollController - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 10), - child: Column( - children: - chatContent.mapIndexed((index, item) { - if (item is DateLabelWidget) { - return item; - } else if (item is MessageModel) { - return Row( - mainAxisAlignment: item.senderId == - ref.read(userProvider)!.id - ? selectedMessages.isNotEmpty - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - if (selectedMessages - .isNotEmpty) // Show check icon only if selected - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: Colors.white, - width: 1), // White border - ), - child: CircleAvatar( - radius: 12, - backgroundColor: - selectedMessages.contains( - item) == - true - ? Colors.green - : Colors.transparent, - child: selectedMessages - .contains(item) == - true - ? const Icon(Icons.check, - color: Colors.white, - size: 16) - : const SizedBox(), - ), + ), + ) + : SingleChildScrollView( + controller: + _scrollController, // Use the ScrollController + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 10), + child: Column( + children: chatContent.mapIndexed((index, item) { + if (item is DateLabelWidget) { + return item; + } else if (item is MessageModel) { + return Row( + mainAxisAlignment: item.senderId == + ref.read(userProvider)!.id + ? selectedMessages.isNotEmpty + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + if (selectedMessages + .isNotEmpty) // Show check icon only if selected + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 1), // White border + ), + child: CircleAvatar( + radius: 12, + backgroundColor: selectedMessages + .contains(item) == + true + ? Colors.green + : Colors.transparent, + child: selectedMessages + .contains(item) == + true + ? const Icon(Icons.check, + color: Colors.white, + size: 16) + : const SizedBox(), ), - if (selectedMessages.contains(item)) - const SizedBox(width: 10), - MessageTileWidget( - key: ValueKey( - '${MessageKeys.messagePrefix}${messagesIndex++}'), - messageModel: item, - isSentByMe: item.senderId == - ref.read(userProvider)!.id, - showInfo: type == ChatType.group, - highlights: - _messageMatches[index] ?? - const [MapEntry(0, 0)], - onReply: (message) { - setState(() { - replyMessage = message; - }); - }, - onPin: (message) { - setState(() { - pinnedMessages.contains(message) - ? pinnedMessages - .remove(message) - : pinnedMessages - .add(message); - }); - }, - onPress: selectedMessages.isEmpty - ? null - : () {}, - onLongPress: (message) { - setState(() { - replyMessage = null; - selectedMessages - .contains(message) - ? selectedMessages - .remove(message) - : selectedMessages - .add(message); - }); - }, - onDelete: (msgId, _, message) {}, - onDownloadTap: - (String? filePath) {}, ), - ], - ); - } else { - return const SizedBox.shrink(); - } - }).toList(), - ), + if (selectedMessages.contains(item)) + const SizedBox(width: 10), + MessageTileWidget( + key: ValueKey( + '${MessageKeys.messagePrefix}${messagesIndex++}'), + chatId: chatModel.id ?? '', + messageModel: item, + isSentByMe: item.senderId == + ref.read(userProvider)!.id, + showInfo: type == ChatType.group, + highlights: _messageMatches[index] ?? + const [MapEntry(0, 0)], + onReply: (message) { + setState(() { + replyMessage = message; + }); + }, + onEdit: (_) {}, + onPin: (message) { + setState(() { + pinnedMessages.contains(message) + ? pinnedMessages + .remove(message) + : pinnedMessages.add(message); + }); + }, + onPress: selectedMessages.isEmpty + ? null + : () {}, + onLongPress: (message) { + setState(() { + replyMessage = null; + selectedMessages.contains(message) + ? selectedMessages + .remove(message) + : selectedMessages + .add(message); + }); + }, + onDownloadTap: (String? filePath) {}, + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }).toList(), ), ), - ), - GestureDetector( - child: Container( - width: double.infinity, - color: Palette.secondary, - child: const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: Text( - 'UNPIN ALL MESSAGES', - style: TextStyle(color: Palette.primary), ), + ), + GestureDetector( + child: Container( + width: double.infinity, + color: Palette.secondary, + child: const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Text( + 'UNPIN ALL MESSAGES', + style: TextStyle(color: Palette.primary), ), - )), - ) - ], - ), - ], - )); + ), + )), + ) + ], + ), + ], + ), + ); } } diff --git a/lib/features/chat/view/widget/bottom_input_bar_widget.dart b/lib/features/chat/view/widget/bottom_input_bar_widget.dart index cc0ecb28..e1801dbf 100644 --- a/lib/features/chat/view/widget/bottom_input_bar_widget.dart +++ b/lib/features/chat/view/widget/bottom_input_bar_widget.dart @@ -19,6 +19,7 @@ import 'package:telware_cross_platform/features/chat/view/widget/slide_to_cancel class BottomInputBarWidget extends ConsumerStatefulWidget { final TextEditingController controller; // Accept controller as a parameter + final bool isEditing; final String? chatID; final void Function({ required String contentType, @@ -29,7 +30,8 @@ class BottomInputBarWidget extends ConsumerStatefulWidget { bool? getRecordingPath, bool isMusic, }) sendMessage; - final void Function() removeReply; + final void Function() unreferenceMessages; + final void Function() editMessage; final AudioRecorderService audioRecorderService; const BottomInputBarWidget({ @@ -37,8 +39,10 @@ class BottomInputBarWidget extends ConsumerStatefulWidget { this.chatID, required this.sendMessage, required this.controller, - required this.removeReply, + required this.unreferenceMessages, + required this.editMessage, required this.audioRecorderService, + required this.isEditing, }); @override @@ -254,13 +258,14 @@ class BottomInputBarWidgetState extends ConsumerState { String? filePath = widget.audioRecorderService.resetRecording(); widget.sendMessage(ref: ref, contentType: 'audio', filePath: filePath); } else { - widget.sendMessage(ref: ref, contentType: 'text'); + widget.isEditing ? widget.editMessage() : widget.sendMessage(ref: ref, contentType: 'text'); } - widget.removeReply(); + widget.unreferenceMessages(); } @override Widget build(BuildContext context) { + debugPrint('this is in the input field widget: ${widget.controller.text}'); return Container( padding: EdgeInsets.symmetric( horizontal: widget.audioRecorderService.isRecordingCompleted ? 0 : 10, @@ -436,7 +441,7 @@ class BottomInputBarWidgetState extends ConsumerState { IconButton( padding: const EdgeInsets.only(left: 10), iconSize: 28, - icon: const Icon(Icons.send), + icon: widget.isEditing ? const Icon(Icons.check_circle) : const Icon(Icons.send), color: Palette.accent, onPressed: _onSend, ), diff --git a/lib/features/chat/view/widget/chat_messages_list.dart b/lib/features/chat/view/widget/chat_messages_list.dart new file mode 100644 index 00000000..476d8f92 --- /dev/null +++ b/lib/features/chat/view/widget/chat_messages_list.dart @@ -0,0 +1,167 @@ +// ignore_for_file: must_be_immutable + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:telware_cross_platform/core/constants/keys.dart'; +import 'package:telware_cross_platform/core/models/message_model.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; +import 'package:telware_cross_platform/core/utils.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; +import 'package:telware_cross_platform/features/chat/providers/chat_provider.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/date_label_widget.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/message_tile_widget.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; + +class ChatMessagesList extends ConsumerStatefulWidget { + ChatMessagesList({ + super.key, + required this.scrollController, + required this.chatContent, + required this.selectedMessages, + required this.type, + required this.messageMatches, + required this.replyMessage, + required this.chatId, + required this.pinnedMessages, + required this.messages, + required this.updateChatMessages, + required this.onPin, + required this.onLongPress, + required this.onReply, + required this.onEdit, + }); + + final ScrollController scrollController; + final ChatType type; + final String? chatId; + List chatContent; + List messages; + List selectedMessages; + List pinnedMessages; + Map>> messageMatches; + MessageModel? replyMessage; + final Function(List messages) updateChatMessages; + final Function(MessageModel message) onPin; + final Function(MessageModel message) onLongPress; + final Function(MessageModel message) onReply; + final Function(MessageModel message) onEdit; + + @override + ConsumerState createState() => + _ChatMessagesListState(); +} + + +class _ChatMessagesListState extends ConsumerState { + var messagesIndex = 0; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + reverse: true, + controller: widget.scrollController, // Use the ScrollController + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + child: Column( + children: widget.chatContent.mapIndexed((index, item) { + if (item is DateLabelWidget) { + return item; + } else if (item is MessageModel) { + int parentIndex = -1; + MessageModel? parentMessage; + if (item.parentMessage != null && + item.parentMessage!.isNotEmpty) { + parentIndex = widget.messages + .indexWhere((msg) => msg.id == item.parentMessage); + } + if (parentIndex >= 0) { + parentMessage = widget.messages[parentIndex]; + } + return Row( + mainAxisAlignment: item.senderId == ref.read(userProvider)!.id + ? widget.selectedMessages.isNotEmpty + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end + : MainAxisAlignment.start, + children: [ + if (widget.selectedMessages + .isNotEmpty) // Show check icon only if selected + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, width: 1), // White border + ), + child: CircleAvatar( + radius: 12, + backgroundColor: + widget.selectedMessages.contains(item) == true + ? Colors.green + : Colors.transparent, + child: widget.selectedMessages.contains(item) == true + ? const Icon(Icons.check, + color: Colors.white, size: 16) + : const SizedBox(), + ), + ), + if (widget.selectedMessages.contains(item)) + const SizedBox(width: 10), + MessageTileWidget( + key: ValueKey( + '${MessageKeys.messagePrefix}${messagesIndex++}'), + messageModel: item, + chatId: widget.chatId ?? '', + isSentByMe: item.senderId == ref.read(userProvider)!.id, + showInfo: widget.type == ChatType.group, + highlights: + widget.messageMatches[index] ?? const [MapEntry(0, 0)], + onDownloadTap: (String? filePath) { + onMediaDownloaded(filePath, item.localId, widget.chatId); + }, + onReply: widget.onReply, + onEdit: widget.onEdit, + onPin: widget.onPin, + onPress: widget.selectedMessages.isEmpty ? null : () {}, + onLongPress: widget.onLongPress, + parentMessage: parentMessage, + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }).toList(), + ), + ), + ); + } + + //---------------------------------------------------------------------------- + //-------------------------------Media---------------------------------------- + + void onMediaDownloaded( + String? filePath, String? messageLocalId, String? chatId) { + if (filePath == null) { + showToastMessage('File has been deleted ask the sender to resend it'); + return; + } + if (messageLocalId == null) { + showToastMessage('File does not exist please upload it again'); + return; + } + if (chatId == null) { + showToastMessage('Chat ID is missing'); + return; + } + debugPrint("Downloaded file path: $filePath"); + debugPrint("Message local ID: $messageLocalId"); + debugPrint("Chat ID: $chatId"); + ref + .read(chattingControllerProvider) + .editMessageFilePath(chatId, messageLocalId, filePath); + List messages = + ref.watch(chatProvider(chatId))?.messages ?? []; + widget.updateChatMessages(messages); + } +} diff --git a/lib/features/chat/view/widget/delete_popup_menu.dart b/lib/features/chat/view/widget/delete_popup_menu.dart index 1e0bbc7d..450c5bd3 100644 --- a/lib/features/chat/view/widget/delete_popup_menu.dart +++ b/lib/features/chat/view/widget/delete_popup_menu.dart @@ -1,70 +1,62 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; -class DeletePopUpMenu extends StatelessWidget { - final WidgetRef ref; +class DeletePopUpMenu extends ConsumerWidget { final String chatId; final String messageId; const DeletePopUpMenu({ super.key, - required this.ref, required this.chatId, required this.messageId, }); @override - Widget build(BuildContext context) { - return PopupMenuButton( - padding: EdgeInsets.zero, - color: Palette.secondary, - icon: const Icon(Icons.menu), - onSelected: (String result) { - if (result == 'delete') { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text("Confirm Deletion"), - content: const Text("Are you sure you want to delete this Message?"), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text("Cancel"), - ), - TextButton( - onPressed: () { - ref.read(chattingControllerProvider).deleteMsg( - messageId, - chatId, - DeleteMessageType.all, - ); - Navigator.pop(context); - }, - child: const Text( - "Delete", - style: TextStyle(color: Colors.white), - ), - ), - ], - ); - }, - ); - } - }, - itemBuilder: (BuildContext context) { - return [ - const PopupMenuItem( - value: 'delete', - child: Text('Delete'), + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + title: const Text("Confirm Deletion"), + content: const Text("Are you sure you want to delete this Message?"), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("Cancel"), + ), + TextButton( + onPressed: () { + ref.read(chattingControllerProvider).deleteMsg( + messageId, + chatId, + DeleteMessageType.all, + ); + Navigator.pop(context); + }, + child: const Text( + "Delete", + style: TextStyle(color: Colors.white), ), - ]; - }, + ), + ], ); } } + +Future showDeleteMessageAlert({ + required BuildContext context, + required String msgId, + required String chatId, +}) { + /// the msgId could be the id or the local id, whichever is available + return showDialog( + context: context, + builder: (context) { + return DeletePopUpMenu( + chatId: chatId, + messageId: msgId, + ); + }, + ); +} \ No newline at end of file diff --git a/lib/features/chat/view/widget/floating_menu_overlay.dart b/lib/features/chat/view/widget/floating_menu_overlay.dart index fdb9b865..bddb2528 100644 --- a/lib/features/chat/view/widget/floating_menu_overlay.dart +++ b/lib/features/chat/view/widget/floating_menu_overlay.dart @@ -10,6 +10,7 @@ class FloatingMenuOverlay extends StatelessWidget { final VoidCallback onPin; final VoidCallback onEdit; final VoidCallback onDelete; + final bool isSentByMe; final bool pinned; const FloatingMenuOverlay({ @@ -21,9 +22,12 @@ class FloatingMenuOverlay extends StatelessWidget { required this.onPin, required this.onEdit, required this.onDelete, + required this.isSentByMe, required this.pinned, }); + final textStyle = const TextStyle(fontSize: 14); + @override Widget build(BuildContext context) { return Stack( @@ -38,90 +42,70 @@ class FloatingMenuOverlay extends StatelessWidget { // Floating menu Positioned( top: MediaQuery.of(context).size.height * 0.4, - left: MediaQuery.of(context).size.width * 0.1, - right: MediaQuery.of(context).size.width * 0.1, + left: MediaQuery.of(context).size.width * 0.25, + right: MediaQuery.of(context).size.width * 0.25, child: Material( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Emoji Row - Container( - padding: - const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - decoration: BoxDecoration( - color: Palette.secondary, - borderRadius: BorderRadius.circular(30), - ), - child: const Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text('👍'), - Text('👎'), - Text('❤️'), - Text('🔥'), - Text('🥰'), - Text('👏'), - Text('😁'), - ], - ), + // color: Colors.green, + child: Center( + child: Container( + // width: 200, + decoration: BoxDecoration( + color: Palette.secondary, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], ), - const SizedBox(height: 5), - // Menu Options - Center( - child: Container( - width: MediaQuery.of(context).size.width * 0.5, - decoration: BoxDecoration( - color: Palette.secondary, - borderRadius: BorderRadius.circular(10), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 5), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.reply), + trailing: const Text('Reply'), + leadingAndTrailingTextStyle: textStyle, + onTap: onReply, + ), + ListTile( + leading: const Icon(Icons.copy), + trailing: const Text('Copy'), + leadingAndTrailingTextStyle: textStyle, + onTap: onCopy, + ), + ListTile( + leading: const Icon(FontAwesomeIcons.share), + trailing: const Text('Forward'), + leadingAndTrailingTextStyle: textStyle, + onTap: onForward, + ), + ListTile( + leading: const Icon(Icons.push_pin_outlined), + trailing: + pinned ? const Text('Unpin') : const Text('Pin'), + leadingAndTrailingTextStyle: textStyle, + onTap: onPin, + ), + if (isSentByMe) + ListTile( + leading: const Icon(Icons.edit), + trailing: const Text('Edit'), + leadingAndTrailingTextStyle: textStyle, + onTap: onEdit, ), - ], - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Column( - children: [ - ListTile( - leading: const Icon(Icons.reply), - title: const Text('Reply'), - onTap: onReply, - ), - ListTile( - leading: const Icon(Icons.copy), - title: const Text('Copy'), - onTap: onCopy, - ), - ListTile( - leading: const Icon(FontAwesomeIcons.share), - title: const Text('Forward'), - onTap: onForward, - ), - ListTile( - leading: Icon(Icons.push_pin_outlined) , - title: pinned ? const Text('Unpin') : const Text('Pin'), - onTap: onPin, - ), - ListTile( - leading: const Icon(Icons.edit), - title: const Text('Edit'), - onTap: onEdit, - ), - ListTile( - leading: const Icon(Icons.delete), - title: const Text('Delete'), - onTap: onDelete, - ), - ], + ListTile( + leading: const Icon(Icons.delete), + trailing: const Text('Delete'), + leadingAndTrailingTextStyle: textStyle, + onTap: onDelete, ), - ), + ], ), ), - ], + ), ), ), ), diff --git a/lib/features/chat/view/widget/member_tile_widget.dart b/lib/features/chat/view/widget/member_tile_widget.dart index 0f626363..ed0fee2c 100644 --- a/lib/features/chat/view/widget/member_tile_widget.dart +++ b/lib/features/chat/view/widget/member_tile_widget.dart @@ -20,7 +20,7 @@ class MemberTileWidget extends StatelessWidget { this.moreInfo, this.onTap, this.showDivider = true, - this.showSelected = true, + this.showSelected = false, }); @override diff --git a/lib/features/chat/view/widget/message_tile_widget.dart b/lib/features/chat/view/widget/message_tile_widget.dart index b219b638..380072a9 100644 --- a/lib/features/chat/view/widget/message_tile_widget.dart +++ b/lib/features/chat/view/widget/message_tile_widget.dart @@ -1,25 +1,31 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; + import 'package:telware_cross_platform/core/constants/keys.dart'; import 'package:telware_cross_platform/core/models/message_model.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/utils.dart'; +import 'package:telware_cross_platform/core/view/widget/highlight_text_widget.dart'; import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/chat/view/widget/audio_message_widget.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/delete_popup_menu.dart'; import 'package:telware_cross_platform/features/chat/view/widget/document_message_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/image_message_widget.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/parent_message.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/sender_name_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/sticker_message_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/video_player_widget.dart'; -import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; -import 'package:telware_cross_platform/core/view/widget/highlight_text_widget.dart'; -import '../../../../core/models/user_model.dart'; + import '../screens/create_chat_screen.dart'; import 'floating_menu_overlay.dart'; class MessageTileWidget extends ConsumerWidget { final MessageModel messageModel; + final String chatId; final bool isSentByMe; final bool showInfo; final Color nameColor; @@ -27,27 +33,29 @@ class MessageTileWidget extends ConsumerWidget { final List> highlights; final void Function(String?) onDownloadTap; final Function(MessageModel) onReply; + final Function(MessageModel) onEdit; final Function(MessageModel) onLongPress; final Function(MessageModel) onPin; - final Function(String, String, DeleteMessageType) onDelete; final Function()? onPress; final MessageModel? parentMessage; - const MessageTileWidget( - {super.key, - required this.messageModel, - required this.isSentByMe, - this.showInfo = false, - this.nameColor = Palette.primary, - this.imageColor = Palette.primary, - this.highlights = const [], - required this.onDownloadTap, - required this.onReply, - required this.onLongPress, - required this.onPress, - required this.onPin, - this.parentMessage, - required this.onDelete}); + const MessageTileWidget({ + super.key, + required this.messageModel, + required this.chatId, + required this.isSentByMe, + this.showInfo = false, + this.nameColor = Palette.primary, + this.imageColor = Palette.primary, + this.highlights = const [], + required this.onDownloadTap, + required this.onReply, + required this.onEdit, + required this.onLongPress, + required this.onPress, + required this.onPin, + this.parentMessage, + }); // Function to format timestamp to "hh:mm AM/PM" String formatTimestamp(DateTime timestamp) { @@ -56,7 +64,8 @@ class MessageTileWidget extends ConsumerWidget { } Widget textMessage(keyValue, ref) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + return Column(crossAxisAlignment: CrossAxisAlignment.start, + children: [ SenderNameWidget( keyValue, nameColor, @@ -64,96 +73,8 @@ class MessageTileWidget extends ConsumerWidget { isSentByMe: isSentByMe, userId: messageModel.senderId, ), - parentMessage != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // First message - Container( - decoration: BoxDecoration( - gradient: isSentByMe - ? LinearGradient( - colors: [ - Color.lerp(Colors.deepPurpleAccent, Colors.white, - 0.4) ?? - Colors.black, - // Increase brightness by using 0.4 - Color.lerp(Colors.deepPurpleAccent, Colors.white, - 0.2) ?? - Colors.deepPurpleAccent, - // Slightly brighten the bottom color - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ) - : LinearGradient( - colors: [ - Color.lerp( - Palette.secondary, Colors.white, 0.4) ?? - Colors.black, - // Increase brightness by using 0.4 - Color.lerp( - Palette.secondary, Colors.white, 0.2) ?? - Palette.secondary, - // Slightly brighten the bottom color - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - borderRadius: const BorderRadius.all( - Radius.circular(16), - ), - ), - padding: const EdgeInsets.all(3), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FutureBuilder( - future: ref - .read(chatsViewModelProvider.notifier) - .getUser(messageModel.senderId), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - return const CircularProgressIndicator(); // Show loading spinner - } else if (snapshot.hasError) { - return Text( - 'Error: ${snapshot.error}', - style: const TextStyle( - color: Colors.red, - ), - ); - } else if (snapshot.hasData) { - return Text( - snapshot.data!.username ?? '', - style: const TextStyle( - color: Palette.primaryText, - fontSize: 16, - ), - ); - } else { - return const Text( - 'No data', - style: TextStyle( - color: Colors.grey, - ), - ); - } - }, - ), - Text( - parentMessage?.content?.toJson()['text'] ?? "", - style: const TextStyle( - color: Palette.primaryText, - fontSize: 16, - ), - ), - ], - ), - ), - ], - ) - : const SizedBox(), + if (parentMessage != null) + ParentMessage(parentMessage: parentMessage), Wrap( children: [ SenderNameWidget( @@ -197,6 +118,11 @@ class MessageTileWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final keyValue = (key as ValueKey).value; + bool isPinned = messageModel.isPinned; + bool isEdited = messageModel.isEdited; + String text = messageModel.content?.toJson()['text'] ?? ""; + if (text.length <= 35 && isPinned) text += ' '; + if (text.length <= 35 && isEdited) text += ' '; Alignment messageAlignment = isSentByMe ? Alignment.centerRight : Alignment.centerLeft; IconData messageState = getMessageStateIcon(messageModel); @@ -217,9 +143,11 @@ class MessageTileWidget extends ConsumerWidget { }, onTap: onPress == null ? () { + SystemChannels.textInput.invokeMethod('TextInput.hide'); late OverlayEntry overlayEntry; overlayEntry = OverlayEntry( builder: (context) => FloatingMenuOverlay( + isSentByMe: messageModel.senderId == ref.read(userProvider)!.id, onDismiss: () { overlayEntry.remove(); }, @@ -241,9 +169,13 @@ class MessageTileWidget extends ConsumerWidget { }, onEdit: () { overlayEntry.remove(); + onEdit(messageModel); }, onDelete: () { overlayEntry.remove(); + final msgId = messageModel.id ?? messageModel.localId; + showDeleteMessageAlert( + context: context, msgId: msgId, chatId: chatId); }, pinned: messageModel.isPinned, ), @@ -254,8 +186,10 @@ class MessageTileWidget extends ConsumerWidget { onLongPress(messageModel); }, child: Container( - margin: const EdgeInsets.symmetric(vertical: 5), - padding: EdgeInsets.all(mediaMessage ? 3 : 12), + margin: const EdgeInsets.symmetric(vertical: 2), + padding: EdgeInsets.symmetric( + horizontal: mediaMessage ? 3 : 12, + vertical: mediaMessage ? 3 : 7), constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75), decoration: BoxDecoration( @@ -279,33 +213,55 @@ class MessageTileWidget extends ConsumerWidget { messageModel.messageContentType, keyValue, ref), // The timestamp is always in the bottom-right corner if there's space Positioned( - bottom: 5, - right: 5, + bottom: 0, + right: 0, child: Container( - padding: const EdgeInsets.only(top: 5), - // Add some space above the timestamp - child: Row( - children: [ - Text( - key: ValueKey( - '$keyValue${MessageKeys.messageTimePostfix.value}'), - formatTimestamp(messageModel.timestamp), - style: const TextStyle( - fontSize: 11, - color: Palette.primaryText, - ), + padding: const EdgeInsets.only(top: 5), + // Add some space above the timestamp + child: Row( + children: [ + if (isPinned) ...[ + Transform.rotate( + angle: 45 * + (3.141592653589793 / + 180), // Convert degrees to radians + child: const Icon(Icons.push_pin_rounded, size: 12), ), - if (isSentByMe) ...[ - const SizedBox(width: 4), - Icon( - key: ValueKey( - '$keyValue${MessageKeys.messageStatusPostfix.value}'), - messageState, - size: 12, - color: Palette.primaryText), - ] + const SizedBox( + width: 2, + ) + ], + if (isEdited) ...[ + const Text("edited", + style: TextStyle( + fontSize: 11, + color: Palette.primaryText, + )), + const SizedBox( + width: 2, + ) ], - )), + Text( + key: ValueKey( + '$keyValue${MessageKeys.messageTimePostfix.value}'), + formatTimestamp(messageModel.timestamp), + style: const TextStyle( + fontSize: 11, + color: Palette.primaryText, + ), + ), + if (isSentByMe) ...[ + const SizedBox(width: 4), + Icon( + key: ValueKey( + '$keyValue${MessageKeys.messageStatusPostfix.value}'), + messageState, + size: 12, + color: Palette.primaryText), + ] + ], + ), + ), ), ], ), @@ -369,62 +325,3 @@ class MessageTileWidget extends ConsumerWidget { } } -class SenderNameWidget extends ConsumerStatefulWidget { - final bool showInfo, isSentByMe; - final String userId; - final dynamic keyValue; - final Color nameColor; - - const SenderNameWidget( - this.keyValue, - this.nameColor, { - super.key, - required this.showInfo, - required this.isSentByMe, - required this.userId, - }); - - @override - ConsumerState createState() => - _SenderNameWidgetState(); -} - -class _SenderNameWidgetState extends ConsumerState { - bool showName = false; - String otherUserName = ''; - - @override - void initState() { - super.initState(); - _getName(); - } - - Future _getName() async { - if (widget.showInfo && !widget.isSentByMe) { - final user = (await ref - .read(chatsViewModelProvider.notifier) - .getUser(widget.userId)); - setState(() { - otherUserName = '${user!.screenFirstName} ${user.screenLastName}'; - showName = true; - }); - } - } - - @override - Widget build(BuildContext context) { - Widget senderNameWidget = showName - ? Text( - key: ValueKey( - '${widget.keyValue}${MessageKeys.messageSenderPostfix.value}'), - otherUserName, - style: TextStyle( - fontWeight: FontWeight.bold, - color: widget.nameColor, - fontSize: 12, - ), - ) - : const SizedBox.shrink(); - return senderNameWidget; - } -} diff --git a/lib/features/chat/view/widget/new_chat_screen_sticker.dart b/lib/features/chat/view/widget/new_chat_screen_sticker.dart new file mode 100644 index 00000000..31bbbce4 --- /dev/null +++ b/lib/features/chat/view/widget/new_chat_screen_sticker.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/core/view/widget/lottie_viewer.dart'; + +class NewChatScreenSticker extends StatelessWidget { + const NewChatScreenSticker({ + super.key, + required String chosenAnimation, + }) : _chosenAnimation = chosenAnimation; + + final String _chosenAnimation; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + width: 210, + margin: const EdgeInsets.symmetric( + horizontal: 24.0), + padding: const EdgeInsets.all(22.0), + decoration: BoxDecoration( + color: const Color.fromRGBO(4, 86, 57, 0.30), + borderRadius: BorderRadius.circular(16.0), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'No messages here yet...', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Palette.primaryText, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + const Text( + 'Send a message or tap the greeting below.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Palette.primaryText, + ), + ), + const SizedBox(height: 20), + LottieViewer( + path: _chosenAnimation, + width: 100, + height: 100, + isLooping: true, + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/chat/view/widget/parent_message.dart b/lib/features/chat/view/widget/parent_message.dart new file mode 100644 index 00000000..1d6a7e48 --- /dev/null +++ b/lib/features/chat/view/widget/parent_message.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:telware_cross_platform/core/models/message_model.dart'; +import 'package:telware_cross_platform/features/chat/view/widget/sender_name_widget.dart'; + +class ParentMessage extends StatelessWidget { + const ParentMessage({super.key, required this.parentMessage}); + final MessageModel? parentMessage; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(6), + border: const Border( + left: BorderSide(color: Colors.white, width: 4.0), // White edge + ), + shape: BoxShape.rectangle, + ), + // height: 50, + margin: const EdgeInsets.symmetric(vertical: 3), + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SenderNameWidget( + DateTime.now().millisecondsSinceEpoch, + Colors.white, + showInfo: true, + isSentByMe: false, + userId: parentMessage?.senderId ?? '', + ), + Text( + parentMessage?.content!.getContent() ?? '', + style: const TextStyle(color: Colors.white), + overflow: TextOverflow + .ellipsis, // Trim the text if it exceeds the available space + maxLines: 3, + ) + ], + ), + ); + } +} diff --git a/lib/features/chat/view/widget/reply_widget.dart b/lib/features/chat/view/widget/reply_widget.dart index a08c77cf..c059f789 100644 --- a/lib/features/chat/view/widget/reply_widget.dart +++ b/lib/features/chat/view/widget/reply_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; import '../../../../core/models/message_model.dart'; import '../../../../core/models/user_model.dart'; @@ -7,14 +8,16 @@ import '../../../../core/theme/palette.dart'; import '../../enum/message_enums.dart'; import '../../view_model/chats_view_model.dart'; -class ReplyWidget extends ConsumerWidget { +class ReplyEditFieldHeader extends ConsumerWidget { final Function() onDiscard; final MessageModel message; + final bool isReplyOrEdit; - const ReplyWidget({ + const ReplyEditFieldHeader({ super.key, required this.message, required this.onDiscard, + required this.isReplyOrEdit, }); @override @@ -26,15 +29,17 @@ class ReplyWidget extends ConsumerWidget { padding: const EdgeInsets.all(10), child: Row( children: [ - const Icon(Icons.reply, color: Palette.primary), - const SizedBox(width: 10), - const Icon(Icons.person), + isReplyOrEdit + ? const Icon(Icons.reply, color: Palette.primary) + : const Icon(Icons.edit, color: Palette.primary), const SizedBox(width: 10), + if (isReplyOrEdit) const Icon(Icons.person), + if (isReplyOrEdit) const SizedBox(width: 10), Expanded( child: FutureBuilder( - future: ref + future: isReplyOrEdit ? ref .read(chatsViewModelProvider.notifier) - .getUser(message.senderId), + .getUser(message.senderId) : Future.value(ref.read(userProvider)), builder: (context, snapshot) { // Handle loading, error, and data states if (snapshot.connectionState == ConnectionState.waiting) { @@ -53,28 +58,31 @@ class ReplyWidget extends ConsumerWidget { style: TextStyle(color: Palette.primary), ); } - final userName = snapshot.data!.username; + final userName = + '${snapshot.data!.screenFirstName} ${snapshot.data!.screenLastName}'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Reply to $userName", + isReplyOrEdit ? "Reply to $userName" : "Edit Message", style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Palette.primary, ), overflow: TextOverflow.ellipsis, + maxLines: 1, ), - Text( + if (isReplyOrEdit) Text( message.messageContentType == MessageContentType.text - ? message.content?.toJson()['text'] ?? "" + ? message.content?.getContent() ?? "" : message.messageContentType.content.toUpperCase(), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Palette.accentText, ), + maxLines: 1, overflow: TextOverflow.ellipsis, ), ], diff --git a/lib/features/chat/view/widget/sender_name_widget.dart b/lib/features/chat/view/widget/sender_name_widget.dart new file mode 100644 index 00000000..bfcdda20 --- /dev/null +++ b/lib/features/chat/view/widget/sender_name_widget.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:telware_cross_platform/core/constants/keys.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; + +class SenderNameWidget extends ConsumerStatefulWidget { + final bool showInfo, isSentByMe; + final String userId; + final dynamic keyValue; + final Color nameColor; + + const SenderNameWidget( + this.keyValue, + this.nameColor, { + super.key, + required this.showInfo, + required this.isSentByMe, + required this.userId, + }); + + @override + ConsumerState createState() => + _SenderNameWidgetState(); +} + +class _SenderNameWidgetState extends ConsumerState { + bool showName = false; + String otherUserName = ''; + + @override + void initState() { + super.initState(); + _getName(); + } + + Future _getName() async { + if (widget.showInfo && !widget.isSentByMe) { + final user = (await ref + .read(chatsViewModelProvider.notifier) + .getUser(widget.userId)); + setState(() { + otherUserName = '${user!.screenFirstName} ${user.screenLastName}'; + showName = true; + }); + } + } + + @override + Widget build(BuildContext context) { + Widget senderNameWidget = widget.isSentByMe + ? const SizedBox.shrink() + : Text( + key: ValueKey( + '${widget.keyValue}${MessageKeys.messageSenderPostfix.value}'), + otherUserName, + style: TextStyle( + fontWeight: FontWeight.bold, + color: widget.nameColor, + fontSize: 12, + ), + ); + return senderNameWidget; + } +} \ No newline at end of file diff --git a/lib/features/chat/view_model/chats_view_model.dart b/lib/features/chat/view_model/chats_view_model.dart index e6561a4d..63284fd8 100644 --- a/lib/features/chat/view_model/chats_view_model.dart +++ b/lib/features/chat/view_model/chats_view_model.dart @@ -28,6 +28,11 @@ class ChatsViewModel extends _$ChatsViewModel { return []; } + void clear() { + state = []; + _otherUsers = {}; + } + void setOtherUsers(Map otherUsers) { _otherUsers = otherUsers; } @@ -53,8 +58,9 @@ class ChatsViewModel extends _$ChatsViewModel { } Future getUser(String id) async { + // debugPrint('!!!** called'); if (id == ref.read(userProvider)!.id) { - debugPrint('!!!** returning the current user'); + // debugPrint('!!!** returning the current user'); return ref.read(userProvider); } @@ -72,7 +78,7 @@ class ChatsViewModel extends _$ChatsViewModel { ref.read(chattingControllerProvider).restoreOtherUsers(_otherUsers); } - debugPrint('!!!** returning a user from the other users map: $user'); + // debugPrint('!!!** returning a user from the other users map: $user'); return user; } @@ -87,12 +93,13 @@ class ChatsViewModel extends _$ChatsViewModel { ({ String msgLocalId, String chatId, - }) addSentMessage( - MessageContent content, - String chatId, - MessageType msgType, - MessageContentType msgContentType, - ) { + }) addSentMessage({ + required MessageContent content, + required String chatId, + required MessageType msgType, + required MessageContentType msgContentType, + required String? parentMessageId, + }) { final chatIndex = getChatIndex(chatId); final chat = state[chatIndex]; // todo(ahmed): make sure that new chats are added to the map first @@ -103,15 +110,15 @@ class ChatsViewModel extends _$ChatsViewModel { senderId + DateTime.now().millisecondsSinceEpoch.toString(); final MessageModel msg = MessageModel( - senderId: senderId, - timestamp: DateTime.now(), - content: content, - messageContentType: msgContentType, - messageType: msgType, - userStates: {}, - id: USE_MOCK_DATA ? getUniqueMessageId() : null, - localId: msgLocalId, - ); + senderId: senderId, + timestamp: DateTime.now(), + content: content, + messageContentType: msgContentType, + messageType: msgType, + userStates: {}, + id: USE_MOCK_DATA ? getUniqueMessageId() : null, + localId: msgLocalId, + parentMessage: parentMessageId); chat.messages.add(msg); @@ -126,7 +133,7 @@ class ChatsViewModel extends _$ChatsViewModel { required String chatId, }) { final chatIndex = getChatIndex(chatId); - final chat = chatIndex > 0 ? state[chatIndex] : null; + final chat = chatIndex >= 0 ? state[chatIndex] : null; if (chat != null) { debugPrint('### found the chat'); @@ -143,10 +150,49 @@ class ChatsViewModel extends _$ChatsViewModel { } } + void editMessage({ + required String msgId, + required String content, + required String chatId, + }) { + final chatIndex = getChatIndex(chatId); + final chat = chatIndex >= 0 ? state[chatIndex] : null; + + if (chat != null) { + final msgIndex = chat.messages.indexWhere((msg) => msg.id == msgId); + + if (msgIndex != -1) { + final newMsg = chat.messages[msgIndex].copyWith( + content: (chat.messages[msgIndex].content as TextContent).copyWith( + text: content, + ), + isEdited: true, + ); + chat.messages[msgIndex] = newMsg; + + state = [ + ...state.sublist(0, chatIndex), + chat.copyWith(), + ...state.sublist(chatIndex + 1), + ]; + } + } + } + Future addReceivedMessage(Map response) async { var chatId = response["chatId"] as String; final chatIndex = getChatIndex(chatId); - var chat = chatIndex > 0 ? state[chatIndex] : null; + var chat = chatIndex >= 0 ? state[chatIndex] : null; + + final msgId = response['id'] as String; + + if (chat == null) { + chat = await ref.read(chattingControllerProvider).getChat(chatId); + state.insert(0, chat); + } + + final msgIndex = chat.messages.indexWhere((msg) => msg.id == msgId); + if (msgIndex != -1) return; Map userStates = { ref.read(userProvider)!.id!: MessageState.read @@ -164,54 +210,56 @@ class ChatsViewModel extends _$ChatsViewModel { ); final msg = MessageModel( - id: response['id'], - senderId: response['senderId'], - messageContentType: contentType, - messageType: MessageType.getType(response['type'] ?? 'unknown'), - content: content, - timestamp: response['timestamp'] == null - ? DateTime.parse(response['timestamp']) - : DateTime.now(), - userStates: userStates, - parentMessage: response['parentMessageId'], - isPinned: response['isPinned'], - isForward: response['isForward']); - - if (chat == null) { - chat = await ref.read(chattingControllerProvider).getChat(chatId); - state.insert(0, chat); - } + id: msgId, + senderId: response['senderId'], + messageContentType: contentType, + messageType: MessageType.getType(response['type'] ?? 'unknown'), + content: content, + timestamp: response['timestamp'] == null + ? DateTime.parse(response['timestamp']) + : DateTime.now(), + userStates: userStates, + parentMessage: response['parentMessageId'], + isPinned: response['isPinned'], + isForward: response['isForward'], + ); chat.messages.add(msg); _moveChatToFront(chatIndex, chat); } - void deleteMessage(String msgId, String chatId) { + bool pinMessage(String msgId, String chatId) { final chatIndex = getChatIndex(chatId); - final chat = state[chatIndex]; + final chat = chatIndex >= 0 ? state[chatIndex] : null; + if (chat == null) return false; final msgIndex = chat.messages.indexWhere((msg) => msg.id == msgId); if (msgIndex != -1) { - chat.messages.removeAt(msgIndex); + final newMsg = chat.messages[msgIndex] + .copyWith(isPinned: !(chat.messages[msgIndex].isPinned)); + chat.messages[msgIndex] = newMsg; + state = [ ...state.sublist(0, chatIndex), chat.copyWith(), ...state.sublist(chatIndex + 1), ]; + return newMsg.isPinned; } + + return false; } - void editMessage(String msgId, String chatId, MessageContent content) { + void deleteMessage(String msgId, String chatId) { final chatIndex = getChatIndex(chatId); final chat = state[chatIndex]; - // Find the msg with the specified ID + final msgIndex = chat.messages.indexWhere((msg) => msg.id == msgId); if (msgIndex != -1) { - chat.messages[msgIndex] = - chat.messages[msgIndex].copyWith(content: content); + chat.messages.removeAt(msgIndex); state = [ ...state.sublist(0, chatIndex), chat.copyWith(), diff --git a/lib/features/chat/view_model/chatting_controller.dart b/lib/features/chat/view_model/chatting_controller.dart index 214bc7c1..1745fd48 100644 --- a/lib/features/chat/view_model/chatting_controller.dart +++ b/lib/features/chat/view_model/chatting_controller.dart @@ -84,10 +84,6 @@ class ChattingController { }, ); - final otherUsersMap = { - for (var user in response.users) user.id!: user - }; - // update the local storage chat's info using the response but not override it final chats = _localRepository.getChats(userId); final updatedChats = response.chats.map((chat) { @@ -107,7 +103,7 @@ class ChattingController { }).toList(); _localRepository.setChats(updatedChats, userId); - _localRepository.setOtherUsers(otherUsersMap, userId); + _localRepository.setOtherUsers(response.users, userId); debugPrint('!!! ended the newLoginInit'); } } @@ -132,7 +128,13 @@ class ChattingController { // get the events and give it to the handler from local final events = _localRepository.getEventQueue(_ref.read(userProvider)!.id!); final newEvents = events.map((e) => e.copyWith(controller: this)); - _eventHandler.init(Queue.from(newEvents.toList())); + _eventHandler.init( + Queue.from( + newEvents.toList(), + ), + userId: _ref.read(userProvider)!.id!, + sessionId: _ref.read(tokenProvider)!, + ); } Future newLoginInit() async { @@ -187,17 +189,22 @@ class ChattingController { }, ); - final otherUsersMap = { - for (var user in response.users) user.id!: user - }; - _localRepository.setChats(response.chats, _ref.read(userProvider)!.id!); _localRepository.setOtherUsers( - otherUsersMap, _ref.read(userProvider)!.id!); + response.users, _ref.read(userProvider)!.id!); debugPrint('!!! ended the newLoginInit'); } } + void clear() { + _eventHandler.clear(); + final userId = (_ref.read(userProvider))?.id! ?? ''; + _localRepository.clearChats(userId); + _localRepository.clearOtherUsers(userId); + _localRepository.clearEventQueue(userId); + _ref.read(chatsViewModelProvider.notifier).clear(); + } + /// Send a text message. /// Must specify either the chatID or userID in case of new chat, /// otherwise, it will throw an error @@ -233,9 +240,17 @@ class ChattingController { } } - final identifier = _ref - .read(chatsViewModelProvider.notifier) - .addSentMessage(content, chatID!, msgType, contentType); + final identifier = + _ref.read(chatsViewModelProvider.notifier).addSentMessage( + content: content, + chatId: chatID!, + msgType: msgType, + msgContentType: contentType, + parentMessageId: parentMessgeId, + ); + + debugPrint( + '^^^ new message identifier: ${identifier.chatId} ${identifier.msgLocalId}'); debugPrint( '^^^ new message identifier: ${identifier.chatId} ${identifier.msgLocalId}'); @@ -243,23 +258,22 @@ class ChattingController { _localRepository.setChats( _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); - final msgEvent = SendMessageEvent( - { - 'chatId': chatID, - 'media': null, - 'content': content.getContent(), - 'contentType': contentType.content, - 'parentMessageId': null, - 'senderId': _ref.read(userProvider)!.id, - 'isFirstTime': isChatNew, - 'chatType': chatType.type, - 'isReplay': false, - 'isForward': false, - }, - controller: this, - msgId: identifier.msgLocalId, - chatId: identifier.chatId - ); + final msgEvent = SendMessageEvent({ + 'chatId': chatID, + 'media': content, + 'content': content.getContent(), + 'contentType': contentType.content, + 'parentMessageId': parentMessgeId, + 'senderId': _ref.read(userProvider)!.id, + 'isFirstTime': isChatNew, + 'chatType': chatType.type, + 'isReplay': false, + 'isForward': false, + }, + controller: this, + msgId: identifier.msgLocalId, + chatId: identifier.chatId, + onEventComplete: (Map res) {}); _eventHandler.addEvent(msgEvent); } @@ -270,36 +284,77 @@ class ChattingController { _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); } + + void receiveGroupCreation(Map response) { + // _ref.read(chatsViewModelProvider.notifier).get(response); + // _localRepository.setChats( + // _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); + } + + + void pinMessageClient(String msgId, String chatId) { + final isToPin = + _ref.read(chatsViewModelProvider.notifier).pinMessage(msgId, chatId); + _eventHandler.addEvent( + PinMessageEvent({ + 'chatId': chatId, + 'messageId': msgId, + }, + msgId: msgId, + chatId: chatId, + isToPin: isToPin, + onEventComplete: (Map res) {}), + ); + } + + void pinMessageServer(String msgId, String chatId) { + _ref.read(chatsViewModelProvider.notifier).pinMessage(msgId, chatId); + } + // delete a message - void deleteMsg(String msgID, String chatID, DeleteMessageType deleteType) { - // final msgEvent = DeleteMessageEvent({ - // 'messageId': msgID, - // }, controller: this); + void deleteMsg(String msgId, String chatId, DeleteMessageType deleteType) { + final msgEvent = DeleteMessageEvent({ + 'messageId': msgId, + }, + controller: this, + msgId: msgId, + chatId: chatId, + onEventComplete: (Map res) {}); + // _eventHandler.addEvent(msgEvent); - // _ref.read(chatsViewModelProvider.notifier).deleteMessage(msgID, chatID); - // _localRepository.setChats( - // _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); + _ref.read(chatsViewModelProvider.notifier).deleteMessage(msgId, chatId); + _localRepository.setChats( + _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); } // edit a message - void editMsg(String msgID, String chatID, MessageContent content) { - // final msgEvent = EditMessageEvent({ - // 'chatId': chatID, - // 'senderId': _ref.read(userProvider)!.id, - // 'messageId': msgID, - // 'content': content - // }, controller: this); + void editMsg(String msgId, String chatId, String content) { + final msgEvent = EditMessageEvent( + { + 'chatId': chatId, + 'senderId': _ref.read(userProvider)!.id, + 'messageId': msgId, + 'content': content + }, + controller: this, + msgId: msgId, + chatId: chatId, + onEventComplete: (Map res) {}, + ); - // _eventHandler.addEvent(msgEvent); + _eventHandler.addEvent(msgEvent); - // _ref - // .read(chatsViewModelProvider.notifier) - // .editMessage(msgID, chatID, content); - // _localRepository.setChats( - // _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); + _ref.read(chatsViewModelProvider.notifier).editMessage( + msgId: msgId, + chatId: chatId, + content: content, + ); + + _localRepository.setChats( + _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); } // receive a message @@ -313,6 +368,24 @@ class ChattingController { Future getOtherUser(String id) async { UserModel? user = await _remoteRepository.getOtherUser(_ref.read(tokenProvider)!, id); + + user ??= UserModel( + username: '', + screenFirstName: 'Not', + screenLastName: 'Found', + email: '', + status: '', + bio: '', + maxFileSize: 0, + automaticDownloadEnable: false, + lastSeenPrivacy: '', + readReceiptsEnablePrivacy: false, + storiesPrivacy: '', + picturePrivacy: '', + invitePermissionsPrivacy: '', + phone: '', + id: '', + ); return user; } @@ -433,6 +506,21 @@ class ChattingController { _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); } + void editMessageIdAck({ + required String msgId, + required String content, + required String chatId, + }) { + _ref.read(chatsViewModelProvider.notifier).editMessage( + msgId: msgId, + content: content, + chatId: chatId, + ); + + _localRepository.setChats( + _ref.read(chatsViewModelProvider), _ref.read(userProvider)!.id!); + } + void setEventsQueue(Queue queue) { _localRepository.setEventQueue(queue, _ref.read(userProvider)!.id!); } @@ -508,4 +596,137 @@ class ChattingController { return null; } } + + Future createGroup({ + required String type, + required String name, + required List members, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "type": type, + "name": name, + "members": members, + }; + final msgEvent = CreateGroupEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future deleteGroup({ + required String chatId, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "chatId": chatId, + }; + final msgEvent = DeleteGroupEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future leaveGroup({ + required String chatId, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "chatId": chatId, + }; + final msgEvent = LeaveGroupEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future addMembers({ + required String chatId, + required List members, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "chatId": chatId, + "users": members, + }; + final msgEvent = AddMembersEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future addAdmin({ + required String chatId, + required List members, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "chatId": chatId, + "members": members, + }; + final msgEvent = AddAdminEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future removeMember({ + required String chatId, + required List members, + required Function(Map res) onEventComplete, + }) async { + Map payload = { + "chatId": chatId, + "members": members, + }; + final msgEvent = RemoveMemberEvent(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + _eventHandler.addEvent(msgEvent); + getUserChats(); + return true; + } + + Future setPermissions({ + required String chatId, + required Function(Map res) onEventComplete, + required String type, + required String who, + }) async { + Map payload = { + "chatId": chatId, + "type":type, + "who":who, + }; + final msgEvent = SetPermissions(payload, + controller: this, + msgId: '', + chatId: '', + onEventComplete: onEventComplete); + getUserChats(); + _eventHandler.addEvent(msgEvent); + return true; + } } diff --git a/lib/features/chat/view_model/event_handler.dart b/lib/features/chat/view_model/event_handler.dart index a2ba11d3..0e90a54c 100644 --- a/lib/features/chat/view_model/event_handler.dart +++ b/lib/features/chat/view_model/event_handler.dart @@ -18,7 +18,13 @@ class EventHandler { bool _isProcessing = false; // Flag to control processing loop bool _stopRequested = false; // Flag to request stopping the loop - void init(Queue eventsQueue) { + void init( + Queue eventsQueue, { + required String userId, + required String sessionId, + }) { + _userId = userId; + _sessionId = sessionId; _queue = eventsQueue; _socket.connect( serverUrl: SOCKET_URL, @@ -30,6 +36,12 @@ class EventHandler { processQueue(); } + void clear() { + stopProcessing(); + _queue.clear(); + _socket.disconnect(); + } + void addEvent(MessageEvent event) { debugPrint('!!! event added'); _queue.add(event); @@ -42,6 +54,7 @@ class EventHandler { } void stopProcessing() { + if (!_isProcessing) return; _stopRequested = true; // Gracefully request stopping the loop } @@ -59,12 +72,8 @@ class EventHandler { final currentEvent = _queue.first; if (!_socket.isConnected) { - _socket.connect( - serverUrl: SOCKET_URL, - userId: _userId, - onConnect: _onSocketConnect, - sessionId: _sessionId, - ); + debugPrint('&%^ called the connect from handler loop'); + _socket.onError(); break; } @@ -93,7 +102,7 @@ class EventHandler { } catch (e) { debugPrint('Error processing event: ${currentEvent.runtimeType}, $e'); if (failingCounter == EVENT_FAIL_LIMIT) { - _stopRequested =true; + _stopRequested = true; _socket.onError(); } await Future.delayed(const Duration(seconds: 2)); @@ -109,11 +118,119 @@ class EventHandler { // receive a message _socket.on(EventType.receiveMessage.event, (response) async { try { + debugPrint('/|\\ got a message id: ${response['id']}'); _chattingController.receiveMsg(response); } on Exception catch (e) { debugPrint('!!! Error in recieving a message:\n${e.toString()}'); } }); + // pin a message + _socket.on(EventType.pinMessageServer.event, (response) async { + try { + _chattingController.pinMessageServer( + response['messageId'] as String, response['chatId'] as String); + } on Exception catch (e) { + debugPrint('!!! Error in pinning a message:\n${e.toString()}'); + } + }); + // unpin a message + _socket.on(EventType.unpinMessageServer.event, (response) async { + try { + _chattingController.pinMessageServer( + response['messageId'] as String, response['chatId'] as String); + } on Exception catch (e) { + debugPrint('!!! Error in unpinning a message:\n${e.toString()}'); + } + }); + // edit a message + _socket.on(EventType.editMessageServer.event, (response) async { + try { + debugPrint('#!#! this is a response of edit:'); + _chattingController.editMessageIdAck(chatId: response['chatId'], content: response['content'], msgId: response['id']); + } on Exception catch (e) { + debugPrint('!!! Error in editing a message:\n${e.toString()}'); + } + }); + _socket.on(EventType.receiveCreateGroup.event, (response) async { + try { + debugPrint('/|\\ got a group creation id:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a message:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveCreateGroup.event, (response) async { + try { + debugPrint('/|\\ got a group creation id:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a message:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveDeleteGroup.event, (response) async { + try { + debugPrint('/|\\ got a delete group id:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a event:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveLeaveGroup.event, (response) async { + try { + debugPrint('/|\\ got a leave group id:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a event:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveAddMember.event, (response) async { + try { + debugPrint('/|\\ got a AddMember :'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a event:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveAddAdmin.event, (response) async { + try { + debugPrint('/|\\ got a AddAdmin :'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a event:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveRemoveMember.event, (response) async { + try { + debugPrint('/|\\ got a receiveRemoveMember:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a event:\n${e.toString()}'); + } + }); + + _socket.on(EventType.receiveSetPermissions.event, (response) async { + try { + debugPrint('/|\\ got a receiveSetPermissions:'); + print(response.toString()); + _chattingController.getUserChats(); + } on Exception catch (e) { + debugPrint('!!! Error in receiveSetPermissions a event:\n${e.toString()}'); + } + }); + // todo(ahmed): add the rest of the recieved events } diff --git a/lib/features/groups/models/groups_event_models.dart b/lib/features/groups/models/groups_event_models.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/features/groups/view/screens/add_members_screen.dart b/lib/features/groups/view/screens/add_members_screen.dart new file mode 100644 index 00000000..f6d027a0 --- /dev/null +++ b/lib/features/groups/view/screens/add_members_screen.dart @@ -0,0 +1,229 @@ +import 'dart:math'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; +import 'package:typed_data/typed_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:telware_cross_platform/core/models/chat_model.dart'; +import 'package:telware_cross_platform/features/chat/view/screens/chat_info_screen.dart'; + +import '../../../../core/models/user_model.dart'; +import '../../../../core/providers/user_provider.dart'; +import '../../../../core/theme/palette.dart'; +import '../../../../core/theme/sizes.dart'; +import '../../../../core/view/widget/lottie_viewer.dart'; +import '../../../auth/view/widget/auth_floating_action_button.dart'; +import '../../../chat/view/widget/member_tile_widget.dart'; +import '../../../stories/utils/utils_functions.dart'; +import '../../../user/view_model/user_view_model.dart'; + +class AddMembersScreen extends ConsumerStatefulWidget { + static const String route = '/create-group'; + final String chatId; + const AddMembersScreen({ + super.key, + required this.chatId, + }); + + @override + ConsumerState createState() => _AddMembersScreen(); +} + +class _AddMembersScreen extends ConsumerState + with TickerProviderStateMixin { + final List fullUserChats = []; + late List userChats; + final TextEditingController searchController = TextEditingController(); + + late Future> _usersFuture; + bool _isUserContentSet = false; + final List _selectedUsers = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _usersFuture = ref.read(userViewModelProvider.notifier).fetchUsers(); + }); + }); + } + + @override + void dispose() { + searchController.dispose(); + super.dispose(); + } + + void _toggleSelectUser(UserModel user) { + if (_selectedUsers.contains(user)) { + _selectedUsers.remove(user); + } else { + _selectedUsers.add(user); + } + setState(() {}); + } + + void _createNewGroup() { + // TODO: Implement group creation + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.secondary, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + context.pop(); // Navigate back when pressed + }, + ), + title: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Add Members', + style: TextStyle( + color: Palette.primaryText, + fontSize: Sizes.primaryText, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: TextField( + controller: searchController, + onChanged: _filterView, + decoration: const InputDecoration( + hintText: 'Search for people', + hintStyle: TextStyle(color: Palette.accentText), + prefixIcon: Icon(Icons.search, color: Palette.accentText), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + ), + ), + ), + Expanded( + child: FutureBuilder>( + future: _usersFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasError) { + return Center( + child: Text( + 'Error loading users: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center( + child: LottieViewer( + path: 'assets/tgs/EasterDuck.tgs', + width: 100, + height: 100, + ), + ); + } else { + if (!_isUserContentSet) { + userChats = _generateUsersList(snapshot.data!); + _isUserContentSet = true; + } + return ListView.builder( + shrinkWrap: + true, // To allow proper layout within parent widgets + itemCount: userChats.length, + itemBuilder: (context, index) { + UserModel user = userChats[index]; + return MemberTileWidget( + text: '${user.screenFirstName} ${user.screenLastName}', + subtext: user.status, + imagePath: user.photo, + onTap: () => _toggleSelectUser(user), + showSelected: _selectedUsers.contains(user), + ); + }, + ); + } + }, + ), + ), + ], + ), + floatingActionButton: AuthFloatingActionButton( + onSubmit: () { + print(_selectedUsers); + List userIds = []; + for (var user in _selectedUsers) { + userIds.add(user.id ?? ''); + } + print(widget.chatId); + ref.read(chattingControllerProvider).addMembers( + chatId: widget.chatId, + members: userIds, + onEventComplete: (res) async { + if (res['success'] == true) { + ref.read(chattingControllerProvider).getUserChats(); + Navigator.pop(context); + } else { + debugPrint('Failed to add members to group'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to add members to group'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + }, + ), + ); + } + + List _generateUsersList(List users) { + UserModel myUser = ref.read(userProvider)!; + return users.where((user) => user.id != myUser.id).toList(); + } + + void _filterView(String query) { + List filteredChats = []; + if (query.isEmpty) { + filteredChats = List.from(fullUserChats); + } else { + filteredChats = fullUserChats.where((user) { + String text = + '${user.screenFirstName} ${user.screenLastName}'.toLowerCase(); + return text.contains(query.toLowerCase()); + }).toList(); + } + setState(() { + userChats = filteredChats; + }); + } + + String randomPrivacy() { + List privacyOptions = ['Everyone', 'Contacts', 'Nobody']; + return privacyOptions[Random().nextInt(privacyOptions.length)]; + } + + Future loadAssetImageBytes(String path) async { + try { + ByteData data = await rootBundle.load(path); + return data.buffer.asUint8List(); + } catch (e) { + return null; // Return null if image loading fails + } + } +} diff --git a/lib/features/chat/view/screens/create_group_screen.dart b/lib/features/groups/view/screens/create_group_screen.dart similarity index 73% rename from lib/features/chat/view/screens/create_group_screen.dart rename to lib/features/groups/view/screens/create_group_screen.dart index c1e25893..a03003ed 100644 --- a/lib/features/chat/view/screens/create_group_screen.dart +++ b/lib/features/groups/view/screens/create_group_screen.dart @@ -13,13 +13,14 @@ import 'package:telware_cross_platform/core/view/widget/lottie_viewer.dart'; import 'package:telware_cross_platform/features/auth/view/widget/auth_floating_action_button.dart'; import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart'; import 'package:telware_cross_platform/features/chat/view/widget/member_tile_widget.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/group_creation_details.dart'; import 'package:telware_cross_platform/features/user/view_model/user_view_model.dart'; +import '../../../../core/routes/routes.dart'; + class CreateGroupScreen extends ConsumerStatefulWidget { static const String route = '/create-group'; - final List? forwardedMessages; - - const CreateGroupScreen({super.key, this.forwardedMessages = const []}); + const CreateGroupScreen({super.key}); @override ConsumerState createState() => _CreateGroupScreen(); @@ -82,9 +83,9 @@ class _CreateGroupScreen extends ConsumerState title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'New Group', - style: TextStyle( + style: const TextStyle( color: Palette.primaryText, fontSize: Sizes.primaryText, fontWeight: FontWeight.w500, @@ -119,38 +120,38 @@ class _CreateGroupScreen extends ConsumerState ), ), ), - FutureBuilder>( - future: _usersFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center( - child: - CircularProgressIndicator(), // Show a loading indicator while data is loading - ); - } else if (snapshot.hasError) { - return Center( - child: Text( - 'Error loading users: ${snapshot.error}', - style: const TextStyle(color: Colors.red), - ), - ); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return const Center( - child: LottieViewer( - path: 'assets/tgs/EasterDuck.tgs', - width: 100, - height: 100, - ), - ); - } else { - if (!_isUserContentSet) { - userChats = _generateUsersList(snapshot.data!); - _isUserContentSet = true; - } - - return SingleChildScrollView( - child: Column( - children: List.generate(userChats.length, (index) { + Expanded( + child: FutureBuilder>( + future: _usersFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasError) { + return Center( + child: Text( + 'Error loading users: ${snapshot.error}', + style: const TextStyle(color: Colors.red), + ), + ); + } else if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center( + child: LottieViewer( + path: 'assets/tgs/EasterDuck.tgs', + width: 100, + height: 100, + ), + ); + } else { + if (!_isUserContentSet) { + userChats = _generateUsersList(snapshot.data!); + _isUserContentSet = true; + } + return ListView.builder( + shrinkWrap: true, // To allow proper layout within parent widgets + itemCount: userChats.length, + itemBuilder: (context, index) { UserModel user = userChats[index]; return MemberTileWidget( text: '${user.screenFirstName} ${user.screenLastName}', @@ -159,18 +160,23 @@ class _CreateGroupScreen extends ConsumerState onTap: () => _toggleSelectUser(user), showSelected: _selectedUsers.contains(user), ); - } - ) - ), - ); - } - }, + }, + ); + } + }, + ), ), ], ), floatingActionButton: AuthFloatingActionButton( onSubmit: () { - context.go('/new-group'); + print('fdsafas'); + print(_selectedUsers); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => GroupCreationDetails(members:_selectedUsers,)), + ); + // context.push(GroupCreationDetails.route); }, ), ); @@ -178,7 +184,6 @@ class _CreateGroupScreen extends ConsumerState List _generateUsersList(List users) { UserModel myUser = ref.read(userProvider)!; - // Return other users only return users.where((user) => user.id != myUser.id).toList(); } @@ -192,7 +197,6 @@ class _CreateGroupScreen extends ConsumerState return text.contains(query.toLowerCase()); }).toList(); } - print(filteredChats); setState(() { userChats = filteredChats; }); diff --git a/lib/features/groups/view/screens/edit_group.dart b/lib/features/groups/view/screens/edit_group.dart new file mode 100644 index 00000000..e1dea75a --- /dev/null +++ b/lib/features/groups/view/screens/edit_group.dart @@ -0,0 +1,445 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:telware_cross_platform/features/groups/view/screens/members_screen.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; +import 'dart:typed_data'; +import '../../../../core/constants/keys.dart'; +import '../../../../core/constants/server_constants.dart'; +import '../../../../core/models/chat_model.dart'; +import '../../../../core/models/user_model.dart'; +import '../../../../core/providers/token_provider.dart'; +import '../../../../core/providers/user_provider.dart'; +import '../../../../core/theme/dimensions.dart'; +import '../../../../core/theme/palette.dart'; +import '../../../../core/theme/sizes.dart'; +import '../../../../core/utils.dart'; +import '../../../chat/view/widget/member_tile_widget.dart'; +import '../../../chat/view_model/chats_view_model.dart'; +import '../../../stories/utils/utils_functions.dart'; +import '../../../user/view/screens/blocked_users.dart'; +import '../../../user/view/screens/invites_permissions_screen.dart'; +import '../../../user/view/screens/last_seen_privacy_screen.dart'; +import '../../../user/view/screens/phone_privacy_screen.dart'; +import '../../../user/view/screens/profile_photo_privacy_screen.dart'; +import '../../../user/view/screens/self_destruct_screen.dart'; +import '../../../user/view/widget/settings_option_widget.dart'; +import '../../../user/view/widget/settings_section.dart'; +import '../../../user/view/widget/toolbar_widget.dart'; +import '../widget/emoji_only_picker_widget.dart'; +import 'add_members_screen.dart'; +import 'package:http/http.dart' as http; + + +class EditGroup extends ConsumerStatefulWidget { + static const String route = 'edit-chat'; + final ChatModel chatModel; + const EditGroup({super.key, required this.chatModel}); + + @override + ConsumerState createState() => _EditGroupState(); +} + +class _EditGroupState extends ConsumerState { + late List> profileSections; + final GlobalKey _widgetKey = GlobalKey(); + late FocusNode _textFieldFocusNode; + late FocusNode _descriptionTextFieldFocusNode; + final TextEditingController searchController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + bool _isEmojiKeyboardVisible = false; + bool _quickDeleteVisible = false; + Uint8List? imageBytes; + bool isPublic = true; + bool isOpen = true; + late RenderBox renderBox; + late Offset offset; + + String _makeIntialName(List members) { + String name = ""; + if (ref.read(userProvider)?.screenFirstName == null) { + name += 'noName'; + } else { + name += ref.read(userProvider)!.screenFirstName; + } + for (var user in members) { + name += " and "; + name += user.screenFirstName; + } + return name; + } + + Future> getUsersInfo() async { + final ChatModel chat = widget.chatModel; + final List users = []; + for (final String userId in chat.userIds) { + final UserModel? user = + await ref.read(chatsViewModelProvider.notifier).getUser(userId); + users.add(user); + } + return users; + } + + @override + void initState() { + super.initState(); + imageBytes = widget.chatModel.photoBytes; + searchController.text = widget.chatModel.title; + _textFieldFocusNode = FocusNode(); + _descriptionTextFieldFocusNode = FocusNode(); + _textFieldFocusNode.addListener(() { + if (_textFieldFocusNode.hasFocus) { + setState(() { + _isEmojiKeyboardVisible = false; + }); + } + }); + } + + @override + void dispose() { + _textFieldFocusNode.dispose(); + super.dispose(); + } + + Future pickImageFromGallery() async { + final ImagePicker picker = ImagePicker(); + final XFile? pickedImage = + await picker.pickImage(source: ImageSource.gallery); + + if (pickedImage != null) { + return File(pickedImage.path); + } else { + debugPrint("No image selected."); + return null; + } + } + + void _updateDependencies(UserModel? user) { + profileSections = [ + { + "title": "", + "options": [ + { + "icon": FontAwesomeIcons.heart, + "text": 'Reactions', + "trailing": "All", + "routes": SelfDestructScreen.route + }, + { + "icon": FontAwesomeIcons.key, + "text": 'Permissions', + "trailing": "13/13", + "routes": SelfDestructScreen.route + }, + { + "icon": FontAwesomeIcons.link, + "text": 'Invite Links', + "trailing": "1", + "routes": SelfDestructScreen.route + }, + { + "icon": Icons.group, + "text": 'Members', + "trailing": "3", + "routes": MembersScreen.route, + "extra": widget.chatModel, + }, + ], + "trailing": "" + }, + ]; + } + + Future updatePrivacy() async { + final String url = '$API_URL/chats/privacy/${widget.chatModel.id!}'; + try { + Map data = { + "privacy": isPublic ==true ? "public":"private", + }; + final response = await http.patch( + Uri.parse(url), + headers: { + 'X-Session-Token': ref.read(tokenProvider)??'' + }, + body: json.encode(data), + ); + if (response.statusCode == 200) { + print('Privacy updated successfully: ${response.body}'); + } else { + print('Privacy updated Failed: ${response.body}'); + print('Failed to update privacy: ${response.statusCode}'); + } + } catch (e) { + // Handle errors, such as no internet connection + print('Error occurred: $e'); + } + } + + @override + Widget build(BuildContext context) { + final user = ref.watch(userProvider); + _updateDependencies(user); + + return Scaffold( + appBar: AppBar( + backgroundColor: Palette.secondary, + title: const Text( + 'Edit', + style: TextStyle( + color: Palette.primaryText, + fontSize: Sizes.primaryText, + fontWeight: FontWeight.w500, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + ref.read(chattingControllerProvider).setPermissions( + chatId: widget.chatModel.id!, + type: 'post', + who: isOpen == true ? "everyone" : "admin", + onEventComplete: (res) { + if (res['success'] == true) { + debugPrint('Group premissions set successfully'); + } else { + debugPrint('Failed to Edit group settings'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to Edit group settings'), + backgroundColor: Colors.red, + ), + ); + } + }); + updatePrivacy(); + context.pop(); + }, + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 15, + ), + child: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundImage: + imageBytes != null ? MemoryImage(imageBytes!) : null, + backgroundColor: imageBytes == null + ? getRandomColor(widget.chatModel.title) + : null, + child: imageBytes == null + ? Text( + getInitials(widget.chatModel.title), + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Palette.primaryText, + ), + ) + : null, + ), + const SizedBox(width: 15), + Expanded( + child: Column( + children: [ + TextField( + focusNode: _textFieldFocusNode, + controller: searchController, + decoration: InputDecoration( + border: InputBorder.none, // No border + enabledBorder: InputBorder + .none, // No border when not focused + focusedBorder: + InputBorder.none, // No border when focused + hintStyle: const TextStyle(color: Colors.white54), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 10.0), + suffixIcon: IconButton( + icon: Icon( + _isEmojiKeyboardVisible == true + ? Icons.keyboard + : Icons.emoji_emotions_outlined, + color: Palette.accentText, + ), + onPressed: () { + setState(() { + _isEmojiKeyboardVisible = + !_isEmojiKeyboardVisible; + if (_isEmojiKeyboardVisible) { + FocusScope.of(context).unfocus(); + if (_isEmojiKeyboardVisible) { + FocusScope.of(context) + .unfocus(); // Hide system keyboard + } + } + }); + }, + ), + ), + ), + Container( + width: double.infinity, + height: 2, + color: Palette.primary, + ), + ], + ), + ), + ], + ), + ), + ), + SettingsOptionWidget( + onTap: () async { + File? file = await pickImageFromGallery(); + if (file != null) { + Uint8List localImageBytes = await file.readAsBytes(); + await uploadChatImage( + file, + '$API_URL/chats/picture/${widget.chatModel.id}', + ref.read(tokenProvider) ?? ''); + setState(() { + imageBytes = localImageBytes; + widget.chatModel.copyWith(photoBytes: imageBytes); + ref.read(chattingControllerProvider).getUserChats(); + }); + } + }, + key: ValueKey( + "set-profile-photo${WidgetKeys.settingsOptionSuffix.value}"), + icon: Icons.camera_alt_outlined, + iconColor: Palette.primary, + text: "Set Profile Photo", + color: Palette.primary, + showDivider: true, + ), + Padding( + padding: const EdgeInsets.only(bottom: 15), + child: Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + Icon( + Icons.group, + color: Palette.accentText, + ), + SizedBox(width: 15), + Text( + 'Group Type', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + ), + ) + ], + ), + GestureDetector( + onTap: () { + setState(() { + isPublic = !isPublic; + print(isPublic ? 'Public' : 'Private'); + }); + }, + child: Text( + isPublic == true ? 'Public' : 'Private', + style: const TextStyle( + color: Palette.primary, + ), + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15), + child: Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + Icon( + Icons.key, + color: Palette.accentText, + ), + SizedBox(width: 15), + Text( + 'Who can Post', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + ), + ) + ], + ), + GestureDetector( + onTap: () { + setState(() { + isOpen = !isOpen; + }); + }, + child: Text( + isOpen == true ? 'Anyone' : 'Admins', + style: const TextStyle( + color: Palette.primary, + ), + ), + ), + ], + ), + ), + ), + ), + ...List.generate(profileSections.length, (index) { + final section = profileSections[index]; + final title = section["title"] ?? ""; + final options = section["options"]; + final trailing = section["trailing"] ?? ""; + return Column( + children: [ + SettingsSection( + title: title, + settingsOptions: options, + trailing: trailing, + ), + const SizedBox(height: Dimensions.sectionGaps), + ], + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/features/groups/view/screens/group_creation_details.dart b/lib/features/groups/view/screens/group_creation_details.dart new file mode 100644 index 00000000..05c13db7 --- /dev/null +++ b/lib/features/groups/view/screens/group_creation_details.dart @@ -0,0 +1,587 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:telware_cross_platform/core/constants/server_constants.dart'; +import 'package:telware_cross_platform/core/models/chat_model.dart'; +import 'package:telware_cross_platform/core/models/user_model.dart'; +import 'package:telware_cross_platform/core/providers/token_provider.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; +import 'package:telware_cross_platform/features/stories/view/screens/add_my_image_screen.dart'; + +import '../../../../core/theme/palette.dart'; +import '../../../../core/theme/sizes.dart'; +import '../../../chat/view/screens/chat_screen.dart'; +import '../../../chat/view/widget/member_tile_widget.dart'; +import '../../../chat/view_model/chatting_controller.dart'; +import '../../../stories/utils/utils_functions.dart'; +import '../../../stories/view/widget/pick_from_gallery.dart'; +import '../widget/emoji_only_picker_widget.dart'; + +class GroupCreationDetails extends ConsumerStatefulWidget { + static const String route = 'group-creation-details'; + final List members; + const GroupCreationDetails({super.key, required this.members}); + + @override + ConsumerState createState() => + _GroupCreationDetailsState(); +} + +class _GroupCreationDetailsState extends ConsumerState { + final GlobalKey _widgetKey = GlobalKey(); + late FocusNode _textFieldFocusNode; + final TextEditingController searchController = TextEditingController(); + bool _isEmojiKeyboardVisible = false; + bool _quickDeleteVisible = false; + File? groupImage; + int autoDelete = -1; + late RenderBox renderBox; + late Offset offset; + + String _makeIntialName(List members) { + String name = ""; + if (ref.read(userProvider)?.screenFirstName == null) { + name += 'noName'; + } else { + name += ref.read(userProvider)!.screenFirstName; + } + for (var user in members) { + name += " and "; + name += user.screenFirstName; + } + return name; + } + + @override + void initState() { + super.initState(); + searchController.text = _makeIntialName(widget.members); + _textFieldFocusNode = FocusNode(); + _textFieldFocusNode.addListener(() { + if (_textFieldFocusNode.hasFocus) { + setState(() { + _isEmojiKeyboardVisible = false; + }); + } + }); + } + + @override + void dispose() { + _textFieldFocusNode.dispose(); + super.dispose(); + } + + void showCustomBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return Material( + color: Colors.transparent, + child: Container( + height: 280, + decoration: BoxDecoration( + color: Palette.secondary, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + topRight: Radius.circular(20.0), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 20), + child: Text( + 'Auto-delete after ...', + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + ), + ), + Expanded( + child: ListWheelScrollView( + onSelectedItemChanged: (int index) { + setState(() { + if (index >= 0 && index < 6) { + autoDelete = index + 1; + } else if (index >= 6 && index < 9) { + autoDelete = (index - 5) * 7; + } else if (index >= 9 && index < 15) { + autoDelete = (index - 8) * 30; + } else if (index == 15) { + autoDelete = 365; + } else if (index == 16) { + autoDelete = -1; + } + }); + }, + itemExtent: 50, + children: [ + for (int i = 1; i <= 6; i++) + GestureDetector( + onTap: () {}, + child: ListTile( + title: Center( + child: Text('$i day${i > 1 ? 's' : ''}')), + ), + ), + for (int i = 1; i <= 3; i++) + GestureDetector( + onTap: () {}, + child: ListTile( + title: Center( + child: Text('$i week${i > 1 ? 's' : ''}')), + ), + ), + for (int i = 1; i <= 6; i++) + GestureDetector( + onTap: () {}, + child: ListTile( + title: Center( + child: Text('$i month${i > 1 ? 's' : ''}')), + ), + ), + GestureDetector( + onTap: () {}, + child: ListTile( + title: Center(child: const Text('1 year')), + ), + ), + GestureDetector( + onTap: () {}, + child: ListTile( + title: Center(child: const Text('Disable')), + ), + ), + ], + ), + ), + GestureDetector( + onTap: () { + context.pop(); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), + child: Container( + decoration: BoxDecoration( + color: Palette.primary, + borderRadius: BorderRadius.circular(20)), + width: double.infinity, + height: 50, + child: Center( + child: Text( + 'Set Auto-Delete', + style: TextStyle( + color: Colors.white, + fontSize: 20, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + } + + Future pickImageFromGallery() async { + final ImagePicker picker = ImagePicker(); + final XFile? pickedImage = + await picker.pickImage(source: ImageSource.gallery); + + if (pickedImage != null) { + return File(pickedImage.path); + } else { + debugPrint("No image selected."); + return null; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () async { + print( + 'group name ${searchController.text} \n image ${groupImage} \n members ${widget.members}'); + List membersIds = widget.members + .where((user) => user.id != null) + .map((user) => user.id!) + .toList(); + await ref.read(chattingControllerProvider).createGroup( + type: 'group', + name: searchController.text, + members: membersIds, + onEventComplete: (res) async { + if (res['success'] == true) { + debugPrint('Group created successfully'); + final members = res['data']['members'] as List; + List userIds = members.map((member) => member['user'] as String).toList(); + Uint8List? imageBytes; + if(groupImage!=null) { + uploadChatImage( + groupImage!, '$API_URL/chats/picture/${res['data']['_id']}', + ref.read(tokenProvider) ?? ''); + imageBytes = await groupImage?.readAsBytes(); + } + + final ChatModel chat = ChatModel( + title: res['data']['name'], + userIds: userIds, + type: res['data']['type'] == 'group' ? ChatType.group : ChatType.channel, + messages: [], + photoBytes: imageBytes, + id: res['data']['_id'], + ); + debugPrint('Opening Chat: $chat'); + context.push(ChatScreen.route, extra: chat); + } else { + debugPrint('Failed to create group'); + // Show a SnackBar if group creation failed + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to create group'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + }, + backgroundColor: Palette.primary, + shape: const CircleBorder(), + child: const Icon(Icons.check), + ), + backgroundColor: Palette.background, + appBar: AppBar( + backgroundColor: Palette.secondary, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + context.pop(); + }, + ), + title: const Text( + 'New Group', + style: TextStyle( + color: Palette.primaryText, + fontSize: Sizes.primaryText, + fontWeight: FontWeight.w500, + ), + ), + ), + body: GestureDetector( + onTap: () { + if (_quickDeleteVisible) { + setState(() { + _quickDeleteVisible = false; + print(_quickDeleteVisible); + }); + } + }, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 15), + child: Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 15, + ), + child: Row( + children: [ + GestureDetector( + onTap: () { + setState(() async { + groupImage = await pickImageFromGallery(); + }); + }, + child: CircleAvatar( + backgroundColor: Palette.primary, + radius: 35, + child: groupImage == null + ? const Icon( + Icons.add_a_photo, + size: 30, + ) + : ClipOval( + child: Image.file( + groupImage!, + width: + 70, // Match the CircleAvatar diameter (2 * radius). + height: 70, + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + children: [ + TextField( + focusNode: _textFieldFocusNode, + controller: searchController, + decoration: InputDecoration( + border: InputBorder.none, // No border + enabledBorder: InputBorder + .none, // No border when not focused + focusedBorder: InputBorder + .none, // No border when focused + hintStyle: + const TextStyle(color: Colors.white54), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 10.0), + suffixIcon: IconButton( + icon: Icon( + _isEmojiKeyboardVisible == true + ? Icons.keyboard + : Icons.emoji_emotions_outlined, + color: Palette.accentText, + ), + onPressed: () { + setState(() { + _isEmojiKeyboardVisible = + !_isEmojiKeyboardVisible; + if (_isEmojiKeyboardVisible) { + FocusScope.of(context).unfocus(); + if (_isEmojiKeyboardVisible) { + FocusScope.of(context) + .unfocus(); // Hide system keyboard + } + } + }); + }, + ), + ), + ), + Container( + width: double.infinity, + height: 2, + color: Palette.primary, + ), + ], + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15), + child: Container( + color: Palette.secondary, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 15, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Row( + children: [ + Icon( + Icons.timer, + color: Palette.accentText, + ), + SizedBox(width: 15), + Text( + 'Auto-Delete Messages', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + ), + ) + ], + ), + GestureDetector( + key: _widgetKey, + onTap: () { + setState(() { + WidgetsBinding.instance + .addPostFrameCallback((_) { + RenderBox renderBox = _widgetKey + .currentContext + ?.findRenderObject() as RenderBox; + offset = renderBox.localToGlobal(Offset.zero); + print( + "Absolute position on screen: ${offset.dx}, ${offset.dy}"); + print(_quickDeleteVisible); + _quickDeleteVisible = !_quickDeleteVisible; + }); + }); + }, + child: Text( + autoDelete == -1 ? 'off' : '${autoDelete} day', + style: const TextStyle( + color: Palette.primary, + ), + ), + ) + ], + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 15), + child: Container( + child: const Padding( + padding: EdgeInsets.symmetric( + horizontal: 15, + ), + child: Text( + 'Automatically delete messages in this group for everyone after period of time.', + ), + ), + ), + ), + Expanded( + child: Container( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + '${widget.members.length} members', + style: TextStyle( + color: Palette.primary, + fontWeight: FontWeight.bold), + ), + ), + Expanded( + child: ListView.builder( + shrinkWrap: + true, // To allow proper layout within parent widgets + itemCount: widget.members.length, + itemBuilder: (context, index) { + UserModel user = widget.members[index]; + return MemberTileWidget( + text: + '${user.screenFirstName} ${user.screenLastName}', + subtext: user.status, + imagePath: user.photo, + onTap: () => () {}, + showSelected: false, + ); + }, + ), + ) + ], + ), + ), + ), + ], + ), + if (_isEmojiKeyboardVisible) + Positioned( + bottom: -40, + left: 0, + right: 0, + child: EmojiOnlyPickerWidget( + textEditingController: searchController, + emojiShowing: _isEmojiKeyboardVisible, + ), + ), + if (_quickDeleteVisible) + Positioned( + left: offset.dx, + top: offset.dy, + child: Material( + color: Colors.transparent, + child: Builder( + builder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showMenu( + context: context, + position: + RelativeRect.fromLTRB(offset.dx, offset.dy, 0, 0), + items: [ + PopupMenuItem( + value: 1, + child: const Text('1 day'), + onTap: () { + setState(() { + _quickDeleteVisible = false; + autoDelete = 1; + }); + }, + ), + PopupMenuItem( + value: 2, + child: const Text('1 week'), + onTap: () { + setState(() { + _quickDeleteVisible = false; + autoDelete = 7; + }); + }, + ), + PopupMenuItem( + value: 3, + child: const Text('1 month'), + onTap: () { + setState(() { + _quickDeleteVisible = false; + autoDelete = 30; + }); + }, + ), + PopupMenuItem( + value: 4, + child: const Text('Set custom time'), + onTap: () { + setState(() { + _quickDeleteVisible = false; + showCustomBottomSheet(context); + }); + }, + ), + if (autoDelete != -1) + PopupMenuItem( + value: 5, + child: const Text('Disable'), + onTap: () { + setState(() { + _quickDeleteVisible = false; + autoDelete = -1; + }); + }, + ), + ], + elevation: 8.0, + ); + }); + return Container(); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/groups/view/screens/members_screen.dart b/lib/features/groups/view/screens/members_screen.dart new file mode 100644 index 00000000..078ae46c --- /dev/null +++ b/lib/features/groups/view/screens/members_screen.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:telware_cross_platform/core/providers/user_provider.dart'; + +import '../../../../core/constants/keys.dart'; +import '../../../../core/models/chat_model.dart'; +import '../../../../core/models/user_model.dart'; +import '../../../../core/routes/routes.dart'; +import '../../../../core/theme/palette.dart'; +import '../../../../core/theme/sizes.dart'; +import 'add_members_screen.dart'; +import '../widget/member_tile_with_options.dart'; +import '../../../user/view/widget/settings_option_widget.dart'; +import '../../../chat/view_model/chats_view_model.dart'; +import '../../../chat/view/widget/member_tile_widget.dart'; + +class MembersScreen extends ConsumerStatefulWidget { + static const String route = '/members-screen'; + final ChatModel chatModel; + const MembersScreen({super.key, required this.chatModel}); + + @override + ConsumerState createState() => _MembersScreenState(); +} + +class _MembersScreenState extends ConsumerState { + late final Future> usersInfoFuture; + + Future> getUsersInfo() async { + final ChatModel chat = widget.chatModel; + final List users = []; + for (final String userId in chat.userIds) { + final UserModel? user = + await ref.read(chatsViewModelProvider.notifier).getUser(userId); + users.add(user); + } + return users; + } + + @override + void initState() { + super.initState(); + usersInfoFuture = getUsersInfo(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Palette.secondary, + title: const Text( + 'Members', + style: TextStyle( + color: Palette.primaryText, + fontSize: Sizes.primaryText, + fontWeight: FontWeight.w500, + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + // make the logic + context.pop(); + }, + ), + ], + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsOptionWidget( + onTap: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddMembersScreen( + chatId: widget.chatModel.id??'', + ), + ), + ); + }, + icon: Icons.person_add_outlined, + iconColor: Palette.primary, + text: "Add Member", + color: Palette.primary, + showDivider: true, + ), + SettingsOptionWidget( + onTap: () async {}, + icon: Icons.link, + iconColor: Palette.primary, + text: "Invite via Link", + color: Palette.primary, + showDivider: true, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: Text( + 'Contacts in this group', + style: TextStyle(fontSize: 13), + ), + ), + FutureBuilder( + future: usersInfoFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final List users = + snapshot.data as List; + return Column( + children: [ + for (final UserModel? user in users) ...[ + user == null + ? const SizedBox.shrink() + : Container( + color: Palette.secondary, + child: MemberTileWithOptions( + showMenu: user.id != ref.read(userProvider)?.id + ? true + : false, + imagePath: user.photo, + text: + '${user.screenFirstName} ${user.screenLastName}', + subtext: user.status, + showDivider: false, + onTap: () { + context.push(Routes.userProfile, + extra: user.id); + }, + userId: user.id ?? '', + chatId: widget.chatModel.id ?? "", context: context, + ), + ), + ], + ], + ); + } else if (snapshot.hasError) { + return Center(child: Text('Error: ${snapshot.error}')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/features/groups/view/screens/premissions_screen.dart b/lib/features/groups/view/screens/premissions_screen.dart new file mode 100644 index 00000000..20adbf3a --- /dev/null +++ b/lib/features/groups/view/screens/premissions_screen.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class PremissionsScreen extends StatelessWidget { + const PremissionsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/features/groups/view/widget/emoji_only_picker_widget.dart b/lib/features/groups/view/widget/emoji_only_picker_widget.dart new file mode 100644 index 00000000..8394e045 --- /dev/null +++ b/lib/features/groups/view/widget/emoji_only_picker_widget.dart @@ -0,0 +1,84 @@ +import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; +import 'package:flutter/material.dart'; + +import '../../../../core/theme/palette.dart'; +class EmojiOnlyPickerWidget extends StatelessWidget { + EmojiOnlyPickerWidget({ + super.key, + required this.textEditingController, + required this.emojiShowing, + this.onEmojiSelected, + this.onBackspacePressed, + }); + + final TextEditingController textEditingController; + final bool emojiShowing; + final void Function(Category? category, Emoji? emoji)? onEmojiSelected; + final void Function()? onBackspacePressed; + final _scrollController = ScrollController(); + + Widget tabBarName(String name) { + return Container( + width: 80, + height: 30, + padding: + const EdgeInsets.only(top: 0.0, bottom: 0.0, left: 8.0, right: 8.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30.0), + // color: const Color.fromRGBO(34, 50, 66, 1), + ), + child: Center( + child: Text( + name, + style: const TextStyle(fontSize: 13), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Offstage( + offstage: !emojiShowing, + child: SizedBox( + height: 300, // Adjust height as needed + child: DefaultTabController( + length: 3, // Number of tabs + child: Column( + children: [ + // Tab View + EmojiPicker( + textEditingController: textEditingController, + scrollController: _scrollController, + config: const Config( + checkPlatformCompatibility: true, + emojiViewConfig: EmojiViewConfig( + emojiSizeMax: 28, + backgroundColor: Palette.trinary, + ), + categoryViewConfig: CategoryViewConfig( + backgroundColor: Palette.trinary, + dividerColor: Palette.trinary, + iconColor: Palette.accentText, + extraTab: CategoryExtraTab.SEARCH, + ), + bottomActionBarConfig: BottomActionBarConfig( + backgroundColor: Palette.trinary, + buttonColor: Palette.trinary, + buttonIconColor: Palette.accentText, + showSearchViewButton: false, + ), + searchViewConfig: SearchViewConfig( + backgroundColor: Palette.trinary, + buttonIconColor: Palette.accentText, + ), + ), + onEmojiSelected: onEmojiSelected, + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/groups/view/widget/member_tile_with_options.dart b/lib/features/groups/view/widget/member_tile_with_options.dart new file mode 100644 index 00000000..da4f8510 --- /dev/null +++ b/lib/features/groups/view/widget/member_tile_with_options.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path/path.dart'; +import 'package:telware_cross_platform/core/theme/dimensions.dart'; +import 'package:telware_cross_platform/core/theme/palette.dart'; +import 'package:telware_cross_platform/core/view/widget/profile_avatar_widget.dart'; +import 'dart:typed_data'; + +import '../../../../core/models/chat_model.dart'; +import '../../../chat/view_model/chatting_controller.dart'; + +class MemberTileWithOptions extends ConsumerStatefulWidget { + final String? imagePath; + final String text; + final String subtext; + final String? moreInfo; + final VoidCallback? onTap; + final bool showDivider; + final bool showSelected; + final bool showMenu; + final String userId; + final String chatId; + final BuildContext context; + + const MemberTileWithOptions({ + super.key, + this.imagePath, + required this.text, + required this.subtext, + this.moreInfo, + this.onTap, + this.showDivider = true, + this.showSelected = false, + this.showMenu = true, + required this.userId, + required this.chatId, + required this.context, + }); + + @override + ConsumerState createState() => + _MemberTileWithOptionsState(); +} + +class _MemberTileWithOptionsState extends ConsumerState { + @override + Widget build(BuildContext context) { + return InkWell( + onTap: widget.onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Dimensions.optionsHorizontalPad, vertical: 6.0), + child: Row( + children: [ + Stack( + children: [ + ProfileAvatarWidget( + text: widget.text, + imagePath: widget.imagePath, + ), + if (widget.showSelected) + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + color: Palette.valid, + shape: BoxShape.circle, + border: Border.all( + color: Palette.secondary, + width: 2, + ), + ), + child: Icon( + Icons.check, + color: Palette.primaryText, + size: 13, + ), + ), + ), + ], + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.text, + style: const TextStyle( + fontSize: 16, + color: Palette.primaryText, + ), + ), + const Spacer(), + if (widget.moreInfo != null) + Text( + widget.moreInfo!, + style: const TextStyle( + fontSize: 14, + color: Palette.accent, + ), + ), + ], + ), + Text( + widget.subtext, + style: const TextStyle( + fontSize: 13, + color: Palette.accentText, + ), + textAlign: TextAlign.left, + ), + if (widget.showDivider) + const Divider(color: Palette.secondary), + ], + ), + ), + widget.showMenu + ? PopupMenuButton( + onSelected: (value) { + if (value == 'option1') { + onOption1Selected(); + } else if (value == 'option2') { + onOption2Selected(); + } + }, + itemBuilder: (BuildContext context) { + return [ + const PopupMenuItem( + value: 'option1', + child: Row( + children: [ + Icon( + Icons.shield, + size: 20, + ), + SizedBox(width: 8), + Text('Promote to admin'), + ], + ), + ), + const PopupMenuItem( + value: 'option2', + child: Row( + children: [ + Icon( + Icons.highlight_off_outlined, + size: 20, + color: Colors.red, + ), + SizedBox(width: 8), + Text( + 'Remove from group', + style: TextStyle(color: Colors.red), + ), + ], + ), + ), + ]; + }, + icon: const Icon( + Icons.more_vert, + color: Palette.accent, + ), + ) + : SizedBox(), + ], + ), + ), + ); + } + + Future onOption1Selected() async { + await ref.read(chattingControllerProvider).addAdmin( + chatId: widget.chatId, + members: [widget.userId], + onEventComplete: (res) async { + if (res['success'] == true) { + debugPrint('Group admin successfully'); + } else { + debugPrint('Failed to make admin'); + ScaffoldMessenger.of(widget.context).showSnackBar( + const SnackBar( + content: Text('Failed to make admin'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + } + + Future onOption2Selected() async { + await ref.read(chattingControllerProvider).removeMember( + chatId: widget.chatId, + members: [widget.userId], + onEventComplete: (res) async { + if (res['success'] == true) { + debugPrint('removed successfully'); + } else { + debugPrint('Failed to make admin'); + ScaffoldMessenger.of(widget.context).showSnackBar( + const SnackBar( + content: Text('Failed to remove member'), + backgroundColor: Colors.red, + ), + ); + } + }, + ); + } +} diff --git a/lib/features/home/view/screens/inbox_screen.dart b/lib/features/home/view/screens/inbox_screen.dart index ec66828a..d7c7823c 100644 --- a/lib/features/home/view/screens/inbox_screen.dart +++ b/lib/features/home/view/screens/inbox_screen.dart @@ -33,9 +33,9 @@ class _InboxScreenState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(chattingControllerProvider).init(); }); - ref.read(chattingControllerProvider).getUserChats().then((_) { - setState(() {}); - }); + // ref.read(chattingControllerProvider).getUserChats().then((_) { + // setState(() {}); + // }); } void _scrollListener() { @@ -78,7 +78,7 @@ class _InboxScreenState extends ConsumerState { onRefresh: _refreshPage, child: CustomScrollView( controller: _scrollController, - physics: const BouncingScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverAppBar( backgroundColor: Palette.secondary, diff --git a/lib/features/stories/repository/contacts_remote_repository.dart b/lib/features/stories/repository/contacts_remote_repository.dart index 69b2d296..32870178 100644 --- a/lib/features/stories/repository/contacts_remote_repository.dart +++ b/lib/features/stories/repository/contacts_remote_repository.dart @@ -290,6 +290,11 @@ class ContactsRemoteRepository { try { var response = await request.send(); + var responseBody = await response.stream.bytesToString(); + + if (kDebugMode) { + debugPrint('Response Body: $responseBody'); + } return response.statusCode == 201; } catch (e) { if (kDebugMode) { diff --git a/lib/features/stories/utils/utils_functions.dart b/lib/features/stories/utils/utils_functions.dart index 5774c3d7..e76fdfd8 100644 --- a/lib/features/stories/utils/utils_functions.dart +++ b/lib/features/stories/utils/utils_functions.dart @@ -50,3 +50,33 @@ Future uploadImage(File imageFile, String uploadUrl) async { return false; } } + + +Future uploadChatImage(File imageFile, String uploadUrl, String sessionToken) async { + var uri = Uri.parse(uploadUrl); + var request = http.MultipartRequest('PATCH', uri) + ..headers['X-Session-Token'] = sessionToken; + var multipartFile = await http.MultipartFile.fromPath( + 'file', + imageFile.path, + contentType: MediaType('image', 'jpeg'), + ); + request.files.add(multipartFile); + + try { + final streamedResponse = await request.send(); + final response = await http.Response.fromStream(streamedResponse); + print(response.body); + if (response.statusCode == 201) { + return true; + } else { + debugPrint('Failed to upload image: ${response.statusCode}, ${response.body}'); + return false; + } + } catch (e) { + if (kDebugMode) { + debugPrint('Error occurred: $e'); + } + return false; + } +} \ No newline at end of file diff --git a/lib/features/stories/view/widget/chats_list.dart b/lib/features/stories/view/widget/chats_list.dart index 104a5d2d..73160e11 100644 --- a/lib/features/stories/view/widget/chats_list.dart +++ b/lib/features/stories/view/widget/chats_list.dart @@ -21,8 +21,6 @@ class ChatsList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final chatsList = ref.watch(chatsViewModelProvider); - debugPrint('chat list ${chatsList.toString()}'); - debugPrint('Building chats_list...'); ChatKeys.resetChatTilePrefixSubvalue(); return SliverList( diff --git a/lib/features/stories/view/widget/expanded_stories_section.dart b/lib/features/stories/view/widget/expanded_stories_section.dart index 090c0691..8c11c613 100644 --- a/lib/features/stories/view/widget/expanded_stories_section.dart +++ b/lib/features/stories/view/widget/expanded_stories_section.dart @@ -21,7 +21,7 @@ class ExpandedStoriesSection extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( - height: kIsWeb ? kToolbarHeight : kToolbarHeight + 25, + height: kIsWeb ? kToolbarHeight : kToolbarHeight + 26, ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), diff --git a/lib/features/user/view/screens/settings_screen.dart b/lib/features/user/view/screens/settings_screen.dart index f7b1a39d..f5c22f6e 100644 --- a/lib/features/user/view/screens/settings_screen.dart +++ b/lib/features/user/view/screens/settings_screen.dart @@ -10,6 +10,7 @@ import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/utils.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_state.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; import 'package:telware_cross_platform/features/stories/view/screens/add_my_image_screen.dart'; import 'package:telware_cross_platform/features/user/view/screens/change_number_screen.dart'; import 'package:telware_cross_platform/features/user/view/screens/change_username_screen.dart'; @@ -149,10 +150,11 @@ class _SettingsScreen extends ConsumerState { const SizedBox(width: 16), IconButton( onPressed: () { + ref.read(chattingControllerProvider).clear(); ref.read(authViewModelProvider.notifier).logOut(); context.go(Routes.logIn); }, - icon: const Icon(Icons.more_vert)), + icon: const Icon(Icons.exit_to_app_rounded)), ], flexibleSpace: LayoutBuilder( builder: (context, constraints) { diff --git a/lib/features/user/view/widget/settings_section.dart b/lib/features/user/view/widget/settings_section.dart index 9f732bc1..5c5487d2 100644 --- a/lib/features/user/view/widget/settings_section.dart +++ b/lib/features/user/view/widget/settings_section.dart @@ -35,8 +35,9 @@ class SettingsSection extends StatelessWidget { this.trailingColor = Colors.transparent, }); - void _navigateTo(BuildContext context, String route) { - context.push(route); + void _navigateTo(BuildContext context, String route, Object? extra) { + print('fdsfdsfdsfdsfdsffffffffffffffffffffffff'); + context.push(route, extra: extra); } @override @@ -73,11 +74,12 @@ class SettingsSection extends StatelessWidget { ? ValueKey("${option["key"]}") : null; final String route = option["routes"] ?? ""; + final Object? extra = option["extra"] ?? ""; final bool lockedRoute = route == 'locked'; final onTap = lockedRoute ? () => showToastMessage("Coming Soon...") : route != "" - ? () => _navigateTo(context, route) + ? () => _navigateTo(context, route, extra) : option["onTap"]; return SettingsOptionWidget( key: key, diff --git a/test/features/chat/widgets/message_tile_widget_test.dart b/test/features/chat/widgets/message_tile_widget_test.dart index bc2b59c1..c89ffc6f 100644 --- a/test/features/chat/widgets/message_tile_widget_test.dart +++ b/test/features/chat/widgets/message_tile_widget_test.dart @@ -53,7 +53,7 @@ void main() { onPress: () {}, onDownloadTap: (String? filePath) {}, onPin: (messageModel) {}, - onDelete: (msgId, _, messageType) {}, + onDelete: (msgId, _, messageType) {}, chatId: '', ), ), ),