diff --git a/lib/core/mock/messages_mock.dart b/lib/core/mock/messages_mock.dart index e34c4240..89e9212b 100644 --- a/lib/core/mock/messages_mock.dart +++ b/lib/core/mock/messages_mock.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'package:telware_cross_platform/core/models/message_model.dart'; +import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; // Faker function to generate a list of random MessageModel objects List generateFakeMessages() { @@ -33,7 +34,8 @@ List generateFakeMessages() { // Create a new message MessageModel message = MessageModel( - senderName: random.nextBool() ? "John Doe" : "Jane Smith", + messageType: MessageType.normal, + senderId: random.nextBool() ? "John Doe" : "Jane Smith", content: sampleMessages[random.nextInt(sampleMessages.length)], timestamp: currentDate.add(Duration( hours: random.nextInt(24), diff --git a/lib/core/mock/user_mock.dart b/lib/core/mock/user_mock.dart index 34d94f29..338521d1 100644 --- a/lib/core/mock/user_mock.dart +++ b/lib/core/mock/user_mock.dart @@ -2,7 +2,8 @@ import 'package:telware_cross_platform/core/models/user_model.dart'; final UserModel userMock = UserModel( username: 'mock.user', - screenName: 'Mocka Mocker', + screenFirstName: 'Mocka', + screenLastName: 'Mocker', email: 'mock@gmail.com', status: 'online', bio: 'I am a mocking user', diff --git a/lib/core/models/chat_model.dart b/lib/core/models/chat_model.dart index ce6d76f3..e3946005 100644 --- a/lib/core/models/chat_model.dart +++ b/lib/core/models/chat_model.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:hive/hive.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/stories/utils/utils_functions.dart'; import '../constants/server_constants.dart'; @@ -9,15 +10,6 @@ import 'message_model.dart'; part 'chat_model.g.dart'; -@HiveType(typeId: 5) -enum ChatType { - @HiveField(0) - oneToOne, - @HiveField(1) - group, - @HiveField(2) - channel, -} @HiveType(typeId: 4) class ChatModel { @@ -200,7 +192,7 @@ class ChatModel { userIds: (map['userIds'] as List).cast(), type: ChatType.values.firstWhere( (e) => e.toString().split('.').last == map['type'], - orElse: () => ChatType.oneToOne, + orElse: () => ChatType.private, ), photo: map['photo'] as String?, id: map['id'] as String?, diff --git a/lib/core/models/chat_model.g.dart b/lib/core/models/chat_model.g.dart index de1ed20d..c50e38a0 100644 --- a/lib/core/models/chat_model.g.dart +++ b/lib/core/models/chat_model.g.dart @@ -78,47 +78,3 @@ class ChatModelAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } - -class ChatTypeAdapter extends TypeAdapter { - @override - final int typeId = 5; - - @override - ChatType read(BinaryReader reader) { - switch (reader.readByte()) { - case 0: - return ChatType.oneToOne; - case 1: - return ChatType.group; - case 2: - return ChatType.channel; - default: - return ChatType.oneToOne; - } - } - - @override - void write(BinaryWriter writer, ChatType obj) { - switch (obj) { - case ChatType.oneToOne: - writer.writeByte(0); - break; - case ChatType.group: - writer.writeByte(1); - break; - case ChatType.channel: - writer.writeByte(2); - break; - } - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ChatTypeAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/core/models/message_model.dart b/lib/core/models/message_model.dart index bec33e22..247b8db2 100644 --- a/lib/core/models/message_model.dart +++ b/lib/core/models/message_model.dart @@ -2,44 +2,23 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:hive/hive.dart'; +import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/stories/utils/utils_functions.dart'; import '../constants/server_constants.dart'; part 'message_model.g.dart'; -enum MessageState { - sent, - read, -} - -enum MessageType { - normal, - announcement, - forward; - - static MessageType getType(String type) { - switch (type) { - case 'announcement': - return MessageType.announcement; - case 'forward': - return MessageType.forward; - default: - return MessageType.normal; - } - } -} - @HiveType(typeId: 6) class MessageModel { @HiveField(0) - final String senderName; // todo(ahmed): not needed + final String senderId; @HiveField(1) final String? content; @HiveField(2) final DateTime timestamp; @HiveField(3) - final Duration? autoDeleteDuration; + final DateTime? autoDeleteTimestamp; @HiveField(4) String? id; @HiveField(5) @@ -48,12 +27,15 @@ class MessageModel { Uint8List? photoBytes; @HiveField(7) final Map userStates; + @HiveField(8) + final MessageType messageType; MessageModel({ - required this.senderName, + this.autoDeleteTimestamp, + required this.messageType, + required this.senderId, this.content, required this.timestamp, - this.autoDeleteDuration, this.id, this.photo, this.photoBytes, @@ -63,9 +45,8 @@ class MessageModel { Future _setPhotoBytes() async { if (photo == null || photo!.isEmpty) return; - String url = photo!.startsWith('http') - ? photo! - : '$API_URL_PICTURES/$photo'; + String url = + photo!.startsWith('http') ? photo! : '$API_URL_PICTURES/$photo'; if (url.isEmpty) return; @@ -92,9 +73,11 @@ class MessageModel { bool operator ==(covariant MessageModel other) { if (identical(this, other)) return true; - return other.senderName == senderName && + return other.messageType == messageType && + other.senderId == senderId && other.content == content && other.timestamp == timestamp && + other.autoDeleteTimestamp == autoDeleteTimestamp && other.id == id && other.photo == photo && other.photoBytes == photoBytes && @@ -103,62 +86,69 @@ class MessageModel { @override int get hashCode { - return senderName.hashCode ^ - content.hashCode ^ - timestamp.hashCode ^ - id.hashCode ^ - photo.hashCode ^ - photoBytes.hashCode ^ - userStates.hashCode; + return messageType.hashCode ^ + autoDeleteTimestamp.hashCode ^ + senderId.hashCode ^ + content.hashCode ^ + timestamp.hashCode ^ + id.hashCode ^ + photo.hashCode ^ + photoBytes.hashCode ^ + userStates.hashCode; } @override String toString() { return ('MessageModel(\n' - 'senderName: $senderName,\n' + 'senderId: $senderId,\n' 'content: $content,\n' 'timestamp: $timestamp,\n' - 'autoDeleteDuration: $autoDeleteDuration,\n' + 'autoDeleteTimestamp: $autoDeleteTimestamp,\n' 'id: $id,\n' 'photo: $photo,\n' 'userStates: $userStates,\n' + 'messageType: ${messageType.name},\n' 'isPhotoBytesSet: ${photoBytes != null}\n' ')'); } MessageModel copyWith({ - String? senderName, + String? senderId, String? content, DateTime? timestamp, - Duration? autoDeleteDuration, + DateTime? autoDeleteTimestamp, String? id, String? photo, Uint8List? photoBytes, Map? userStates, + MessageType? messageType, }) { return MessageModel( - senderName: senderName ?? this.senderName, + senderId: senderId ?? this.senderId, content: content ?? this.content, timestamp: timestamp ?? this.timestamp, - autoDeleteDuration: autoDeleteDuration ?? this.autoDeleteDuration, + 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 ); } Map toMap({bool forSender = false}) { final map = { - 'senderName': senderName, + 'senderId': senderId, 'content': content, 'timestamp': timestamp.toIso8601String(), - 'autoDeleteDuration': autoDeleteDuration?.inSeconds, + 'autoDeleteTimestamp': autoDeleteTimestamp?.microsecondsSinceEpoch, 'id': id, 'photo': photo, 'userStates': forSender - ? userStates.map((key, value) => MapEntry(key, value.toString().split('.').last)) + ? userStates.map( + (key, value) => MapEntry(key, value.toString().split('.').last)) : null, + 'messageType': messageType.name }; return map; @@ -166,19 +156,20 @@ class MessageModel { static Future fromMap(Map map) async { final message = MessageModel( - senderName: map['senderName'] as String, - content: map['content'] as String?, + senderId: map['senderId'] as String, + content: map['content'] as String, timestamp: DateTime.parse(map['timestamp'] as String), - autoDeleteDuration: map['autoDeleteDuration'] != null - ? Duration(seconds: map['autoDeleteDuration'] as int) - : null, - id: map['id'] as String?, + id: map['messageId'] as String?, photo: map['photo'] as String?, + messageType: MessageType.getType(map['messageType']), + autoDeleteTimestamp: map['autoDeleteTimeStamp'] != null + ? DateTime.parse(map['autoDeleteTimeStamp']) + : null, userStates: (map['userStates'] as Map?)?.map( - (key, value) => MapEntry( + (key, value) => MapEntry( key, MessageState.values.firstWhere( - (e) => e.toString().split('.').last == value, + (e) => e.toString().split('.').last == value, orElse: () => MessageState.sent, ), ), @@ -188,5 +179,6 @@ class MessageModel { return message; } - String toJson({bool forSender = false}) => json.encode(toMap(forSender: forSender)); + String toJson({bool forSender = false}) => + json.encode(toMap(forSender: forSender)); } diff --git a/lib/core/models/message_model.g.dart b/lib/core/models/message_model.g.dart index 576b7d97..71906b4c 100644 --- a/lib/core/models/message_model.g.dart +++ b/lib/core/models/message_model.g.dart @@ -17,10 +17,11 @@ class MessageModelAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return MessageModel( - senderName: fields[0] as String, + autoDeleteTimestamp: fields[3] as DateTime?, + messageType: fields[8] as MessageType, + senderId: fields[0] as String, content: fields[1] as String?, timestamp: fields[2] as DateTime, - autoDeleteDuration: fields[3] as Duration?, id: fields[4] as String?, photo: fields[5] as String?, photoBytes: fields[6] as Uint8List?, @@ -31,15 +32,15 @@ class MessageModelAdapter extends TypeAdapter { @override void write(BinaryWriter writer, MessageModel obj) { writer - ..writeByte(8) + ..writeByte(9) ..writeByte(0) - ..write(obj.senderName) + ..write(obj.senderId) ..writeByte(1) ..write(obj.content) ..writeByte(2) ..write(obj.timestamp) ..writeByte(3) - ..write(obj.autoDeleteDuration) + ..write(obj.autoDeleteTimestamp) ..writeByte(4) ..write(obj.id) ..writeByte(5) @@ -47,7 +48,9 @@ class MessageModelAdapter extends TypeAdapter { ..writeByte(6) ..write(obj.photoBytes) ..writeByte(7) - ..write(obj.userStates); + ..write(obj.userStates) + ..writeByte(8) + ..write(obj.messageType); } @override diff --git a/lib/core/models/user_model.dart b/lib/core/models/user_model.dart index 3d4b592f..94e1a340 100644 --- a/lib/core/models/user_model.dart +++ b/lib/core/models/user_model.dart @@ -14,7 +14,9 @@ class UserModel { @HiveField(0) final String username; @HiveField(1) - final String screenName; + final String screenFirstName; + @HiveField(16) + final String screenLastName; @HiveField(2) final String email; @HiveField(3) @@ -46,7 +48,8 @@ class UserModel { UserModel({ required this.username, - required this.screenName, + required this.screenFirstName, + required this.screenLastName, required this.email, this.photo, required this.status, @@ -90,7 +93,8 @@ class UserModel { if (identical(this, other)) return true; return other.username == username && - other.screenName == screenName && + other.screenFirstName == screenFirstName && + other.screenLastName == screenLastName && other.email == email && other.photo == photo && other.status == status && @@ -109,7 +113,8 @@ class UserModel { @override int get hashCode { return username.hashCode ^ - screenName.hashCode ^ + screenFirstName.hashCode ^ + screenLastName.hashCode ^ email.hashCode ^ photo.hashCode ^ status.hashCode ^ @@ -127,12 +132,13 @@ class UserModel { @override String toString() { - return 'UserModel(\n username: $username,\n screenName: $screenName,\n email: $email,\n photo: $photo,\n status: $status,\n bio: $bio,\n maxFileSize: $maxFileSize,\n automaticDownloadEnable: $automaticDownloadEnable,\n lastSeenPrivacy: $lastSeenPrivacy,\n readReceiptsEnablePrivacy: $readReceiptsEnablePrivacy,\n storiesPrivacy: $storiesPrivacy,\n picturePrivacy: $picturePrivacy,\n invitePermissionsPrivacy: $invitePermissionsPrivacy,\n phone: $phone,\n id: $id,\n isPhotoBytesSet: ${photoBytes != null}\n)'; + return 'UserModel(\n username: $username,\n screenName: $screenFirstName $screenLastName,\n email: $email,\n photo: $photo,\n status: $status,\n bio: $bio,\n maxFileSize: $maxFileSize,\n automaticDownloadEnable: $automaticDownloadEnable,\n lastSeenPrivacy: $lastSeenPrivacy,\n readReceiptsEnablePrivacy: $readReceiptsEnablePrivacy,\n storiesPrivacy: $storiesPrivacy,\n picturePrivacy: $picturePrivacy,\n invitePermissionsPrivacy: $invitePermissionsPrivacy,\n phone: $phone,\n id: $id,\n isPhotoBytesSet: ${photoBytes != null}\n)'; } UserModel copyWith({ String? username, - String? screenName, + String? screenFirstName, + String? screenLastName, String? email, String? photo, String? status, @@ -150,7 +156,8 @@ class UserModel { }) { return UserModel( username: username ?? this.username, - screenName: screenName ?? this.screenName, + screenFirstName: screenFirstName ?? this.screenFirstName, + screenLastName: screenLastName ?? this.screenLastName, email: email ?? this.email, photo: photo ?? this.photo, status: status ?? this.status, @@ -174,7 +181,8 @@ class UserModel { Map toMap() { return { 'username': username, - 'screenName': screenName, + 'screenFirstName': screenFirstName, + 'screenLastName': screenLastName, 'email': email, 'photo': photo, 'status': status, @@ -192,13 +200,16 @@ class UserModel { } static Future fromMap(Map map) async { - String screenName = (map['screenName'] as String?) ?? ''; - if (screenName.isEmpty) { - screenName = 'No Name'; + String first = (map['screenFirstName'] as String?) ?? ''; + String last = (map['screenFirstName'] as String?) ?? ''; + if (first.isEmpty) { + first = 'No'; + last = 'Name'; } final user = UserModel( username: map['username'] as String, - screenName: screenName, + screenFirstName: first, + screenLastName: last, email: (map['email'] as String?) ?? '', photo: map['photo'] != null ? map['photo'] as String : null, status: map['status'] as String, diff --git a/lib/core/models/user_model.g.dart b/lib/core/models/user_model.g.dart index 42c07559..9b5ce4b8 100644 --- a/lib/core/models/user_model.g.dart +++ b/lib/core/models/user_model.g.dart @@ -18,7 +18,8 @@ class UserModelAdapter extends TypeAdapter { }; return UserModel( username: fields[0] as String, - screenName: fields[1] as String, + screenFirstName: fields[1] as String, + screenLastName: fields[16] as String, email: fields[2] as String, photo: fields[3] as String?, status: fields[4] as String, @@ -39,11 +40,13 @@ class UserModelAdapter extends TypeAdapter { @override void write(BinaryWriter writer, UserModel obj) { writer - ..writeByte(16) + ..writeByte(17) ..writeByte(0) ..write(obj.username) ..writeByte(1) - ..write(obj.screenName) + ..write(obj.screenFirstName) + ..writeByte(16) + ..write(obj.screenLastName) ..writeByte(2) ..write(obj.email) ..writeByte(3) diff --git a/lib/core/routes/routes.dart b/lib/core/routes/routes.dart index 6d053288..79350912 100644 --- a/lib/core/routes/routes.dart +++ b/lib/core/routes/routes.dart @@ -234,8 +234,8 @@ class Routes { GoRoute( path: Routes.chatScreen, builder: (context, state) { - final ChatModel chatModel = state.extra as ChatModel; - return ChatScreen(chatModel: chatModel); + final String chatId = state.extra as String; + return ChatScreen(chatId: chatId); } ), GoRoute( diff --git a/lib/features/auth/view_model/auth_view_model.dart b/lib/features/auth/view_model/auth_view_model.dart index ef5fd839..fb141273 100644 --- a/lib/features/auth/view_model/auth_view_model.dart +++ b/lib/features/auth/view_model/auth_view_model.dart @@ -14,6 +14,7 @@ import 'package:telware_cross_platform/features/auth/repository/auth_local_repos import 'package:telware_cross_platform/features/auth/repository/auth_remote_repository.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_state.dart'; import 'package:telware_cross_platform/core/models/app_error.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; import 'package:url_launcher/url_launcher.dart'; part 'auth_view_model.g.dart'; @@ -69,7 +70,6 @@ class AuthViewModel extends _$AuthViewModel { return token != null && token.isNotEmpty; } - Future signUp({ required String email, required String phone, @@ -156,12 +156,15 @@ class AuthViewModel extends _$AuthViewModel { if (USE_MOCK_DATA) { if (email == userMock.email && password == userMockPassword) { - state = AuthState.authenticated; ref.read(authLocalRepositoryProvider).setUser(userMock); ref.read(userProvider.notifier).update((_) => userMock); ref.read(authLocalRepositoryProvider).setToken(tokenMock); ref.read(tokenProvider.notifier).update((_) => tokenMock); + + await ref.read(chattingControllerProvider).newLoginInit(); + + state = AuthState.authenticated; return; } else { state = AuthState.fail('Invalid email or password'); @@ -179,12 +182,13 @@ class AuthViewModel extends _$AuthViewModel { } else { state = AuthState.fail(appError.error); } - }, (logInResponse) { + }, (logInResponse) async { ref.read(authLocalRepositoryProvider).setUser(logInResponse.user); ref.read(userProvider.notifier).update((_) => logInResponse.user); ref.read(authLocalRepositoryProvider).setToken(logInResponse.token); ref.read(tokenProvider.notifier).update((_) => logInResponse.token); + await ref.read(chattingControllerProvider).newLoginInit(); state = AuthState.authenticated; }); } @@ -324,8 +328,7 @@ class AuthViewModel extends _$AuthViewModel { // try getting updated user data final response = await ref.read(authRemoteRepositoryProvider).getMe(token!); - response.match((appError) { - }, (user) { + response.match((appError) {}, (user) { debugPrint('** getMe is called\nuser supposed to have img'); print(user); ref.read(authLocalRepositoryProvider).setUser(user); diff --git a/lib/features/auth/view_model/auth_view_model.g.dart b/lib/features/auth/view_model/auth_view_model.g.dart index f2b28429..8fa255c2 100644 --- a/lib/features/auth/view_model/auth_view_model.g.dart +++ b/lib/features/auth/view_model/auth_view_model.g.dart @@ -6,7 +6,7 @@ part of 'auth_view_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$authViewModelHash() => r'01f10ab4dfed955d2ac2a7baaa871bd0a8af3650'; +String _$authViewModelHash() => r'58fd41d9cb235c96c86b2c88141ed10c5d14cfca'; /// See also [AuthViewModel]. @ProviderFor(AuthViewModel) diff --git a/lib/features/chat/models/enums.dart b/lib/features/chat/enum/chatting_enums.dart similarity index 50% rename from lib/features/chat/models/enums.dart rename to lib/features/chat/enum/chatting_enums.dart index 48a66217..94f52735 100644 --- a/lib/features/chat/models/enums.dart +++ b/lib/features/chat/enum/chatting_enums.dart @@ -1,3 +1,7 @@ +import 'package:hive/hive.dart'; + +part 'chatting_enums.g.dart'; + enum EventType { sendMessage(event: 'SEND_MESSAGE'), sendAnnouncement(event: 'SEND_ANNOUNCEMENT'), @@ -17,12 +21,26 @@ enum EventType { const EventType({required this.event}); } -enum DeleteMsgType { - sendMessage(name: 'only-me'), - sendAnnouncement(name: 'all') - ; +@HiveType(typeId: 5) +enum ChatType { + @HiveField(0) + private(type: 'private'), + @HiveField(1) + group(type: 'group'), + @HiveField(2) + channel(type: 'channel'); - final String name; + static ChatType getType(String type) { + switch (type) { + case 'group': + return ChatType.group; + case 'channel': + return ChatType.channel; + default: + return ChatType.private; + } + } - const DeleteMsgType({required this.name}); + final String type; + const ChatType({required this.type}); } \ No newline at end of file diff --git a/lib/features/chat/enum/chatting_enums.g.dart b/lib/features/chat/enum/chatting_enums.g.dart new file mode 100644 index 00000000..e5fc2388 --- /dev/null +++ b/lib/features/chat/enum/chatting_enums.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chatting_enums.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ChatTypeAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + ChatType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ChatType.private; + case 1: + return ChatType.group; + case 2: + return ChatType.channel; + default: + return ChatType.private; + } + } + + @override + void write(BinaryWriter writer, ChatType obj) { + switch (obj) { + case ChatType.private: + writer.writeByte(0); + break; + case ChatType.group: + writer.writeByte(1); + break; + case ChatType.channel: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChatTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/chat/enum/message_enums.dart b/lib/features/chat/enum/message_enums.dart new file mode 100644 index 00000000..fc0a2407 --- /dev/null +++ b/lib/features/chat/enum/message_enums.dart @@ -0,0 +1,94 @@ +import 'package:hive/hive.dart'; + +part 'message_enums.g.dart'; + +@HiveType(typeId: 11) +enum MessageState { + @HiveField(0) + sent(text: 'sent'), + @HiveField(1) + read(text: 'read'), + @HiveField(2) + pending(text: 'pending'); + + static MessageState getType(String type) { + switch (type) { + case 'sent': + return MessageState.sent; + case 'read': + return MessageState.read; + default: + return MessageState.pending; + } + } + + final String text; + const MessageState({required this.text}); +} + +@HiveType(typeId: 12) +enum MessageType { + @HiveField(0) + normal(text: 'normal'), + @HiveField(1) + announcement(text: 'announcement'), + @HiveField(2) + forward(text: 'forward'); + + static MessageType getType(String type) { + switch (type) { + case 'announcement': + return MessageType.announcement; + case 'forward': + return MessageType.forward; + default: + return MessageType.normal; + } + } + + final String text; + const MessageType({required this.text}); +} + +enum MessageContentType { + text(content: 'text'), + image(content: 'image'), + gif(content: 'GIF'), + sticker(content: 'sticker'), + audio(content: 'audio'), + video(content: 'video'), + file(content: 'file'), + link(content: 'link'); + + static MessageContentType getType(String type) { + switch (type) { + case 'image': + return MessageContentType.image; + case 'GIF': + return MessageContentType.gif; + case 'sticker': + return MessageContentType.sticker; + case 'audio': + return MessageContentType.audio; + case 'video': + return MessageContentType.video; + case 'file': + return MessageContentType.file; + case 'link': + return MessageContentType.link; + default: + return MessageContentType.text; + } + } + + final String content; + const MessageContentType({required this.content}); +} + +enum DeleteMessageType { + onlyMe(text: 'only-me'), + all(text: 'all'); + + final String text; + const DeleteMessageType({required this.text}); +} \ No newline at end of file diff --git a/lib/features/chat/enum/message_enums.g.dart b/lib/features/chat/enum/message_enums.g.dart new file mode 100644 index 00000000..ddea46d5 --- /dev/null +++ b/lib/features/chat/enum/message_enums.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_enums.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MessageStateAdapter extends TypeAdapter { + @override + final int typeId = 11; + + @override + MessageState read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return MessageState.sent; + case 1: + return MessageState.read; + case 2: + return MessageState.pending; + default: + return MessageState.sent; + } + } + + @override + void write(BinaryWriter writer, MessageState obj) { + switch (obj) { + case MessageState.sent: + writer.writeByte(0); + break; + case MessageState.read: + writer.writeByte(1); + break; + case MessageState.pending: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageStateAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class MessageTypeAdapter extends TypeAdapter { + @override + final int typeId = 12; + + @override + MessageType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return MessageType.normal; + case 1: + return MessageType.announcement; + case 2: + return MessageType.forward; + default: + return MessageType.normal; + } + } + + @override + void write(BinaryWriter writer, MessageType obj) { + switch (obj) { + case MessageType.normal: + writer.writeByte(0); + break; + case MessageType.announcement: + writer.writeByte(1); + break; + case MessageType.forward: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/chat/models/message_event_models.dart b/lib/features/chat/models/message_event_models.dart index 511aee5e..7e3cc77a 100644 --- a/lib/features/chat/models/message_event_models.dart +++ b/lib/features/chat/models/message_event_models.dart @@ -1,7 +1,8 @@ import 'dart:async'; +import 'package:hive/hive.dart'; import 'package:telware_cross_platform/core/services/socket_service.dart'; -import 'package:telware_cross_platform/features/chat/models/enums.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; // todo: create the event classes here @@ -18,13 +19,19 @@ import 'package:telware_cross_platform/features/chat/view_model/chatting_control // recieve msg/reply/draft // edit/delete msg -abstract class MessageEvent { +part 'message_event_models.g.dart'; + +@HiveType(typeId: 7) +class MessageEvent { + @HiveField(0) final dynamic payload; - final ChattingController _controller; - MessageEvent(this._controller, this.payload); + + final ChattingController? _controller; + + MessageEvent(this.payload, {ChattingController? controller}): _controller = controller; Future execute(SocketService socket, - {Duration timeout = const Duration(seconds: 10)}); + {Duration timeout = const Duration(seconds: 10)}) async {return true;} Future _execute( SocketService socket, @@ -50,10 +57,21 @@ abstract class MessageEvent { return completer.future; } + + MessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + }) { + return SendMessageEvent( + payload ?? this.payload, + controller: controller ?? _controller, + ); + } } +@HiveType(typeId: 8) class SendMessageEvent extends MessageEvent { - SendMessageEvent(super._controller, super.payload); + SendMessageEvent(super.payload, {super.controller}); @override Future execute( @@ -66,21 +84,29 @@ class SendMessageEvent extends MessageEvent { ackCallback: (response, timer, completer) { if (!completer.isCompleted) { timer.cancel(); // Cancel the timeout timer - if (response['status'] == 'success') { - completer.complete(true); - // todo(moamen): confirm msg was sent - // _controller.confirmMsgSent() - } else { - completer.complete(false); - } + // todo(moamen): confirm msg was sent + // change from pending to sent + completer.complete(true); } }, ); } + + @override + SendMessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + }) { + return SendMessageEvent( + payload ?? this.payload, + controller: controller ?? _controller, + ); + } } +@HiveType(typeId: 9) class DeleteMessageEvent extends MessageEvent { - DeleteMessageEvent(super._controller, super.payload); + DeleteMessageEvent(super.payload, {super.controller}); @override Future execute( @@ -93,19 +119,27 @@ class DeleteMessageEvent extends MessageEvent { ackCallback: (response, timer, completer) { if (!completer.isCompleted) { timer.cancel(); // Cancel the timeout timer - if (response['status'] == 'success') { - completer.complete(true); - } else { - completer.complete(false); - } + completer.complete(true); } }, ); } + + @override + DeleteMessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + }) { + return DeleteMessageEvent( + payload ?? this.payload, + controller: controller ?? _controller, + ); + } } +@HiveType(typeId: 10) class EditMessageEvent extends MessageEvent { - EditMessageEvent(super._controller, super.payload); + EditMessageEvent(super.payload, {super.controller}); @override Future execute( @@ -118,13 +152,20 @@ class EditMessageEvent extends MessageEvent { ackCallback: (response, timer, completer) { if (!completer.isCompleted) { timer.cancel(); // Cancel the timeout timer - if (response['status'] == 'success') { - completer.complete(true); - } else { - completer.complete(false); - } + completer.complete(true); } }, ); } + + @override + EditMessageEvent copyWith({ + dynamic payload, + ChattingController? controller, + }) { + return EditMessageEvent( + payload ?? this.payload, + controller: controller ?? _controller, + ); + } } diff --git a/lib/features/chat/models/message_event_models.g.dart b/lib/features/chat/models/message_event_models.g.dart new file mode 100644 index 00000000..2218eed8 --- /dev/null +++ b/lib/features/chat/models/message_event_models.g.dart @@ -0,0 +1,143 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_event_models.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class MessageEventAdapter extends TypeAdapter { + @override + final int typeId = 7; + + @override + MessageEvent read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return MessageEvent( + fields[0] as dynamic, + ); + } + + @override + void write(BinaryWriter writer, MessageEvent obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.payload); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageEventAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class SendMessageEventAdapter extends TypeAdapter { + @override + final int typeId = 8; + + @override + SendMessageEvent read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return SendMessageEvent( + fields[0] as dynamic, + ); + } + + @override + void write(BinaryWriter writer, SendMessageEvent obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.payload); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SendMessageEventAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class DeleteMessageEventAdapter extends TypeAdapter { + @override + final int typeId = 9; + + @override + DeleteMessageEvent read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return DeleteMessageEvent( + fields[0] as dynamic, + ); + } + + @override + void write(BinaryWriter writer, DeleteMessageEvent obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.payload); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DeleteMessageEventAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class EditMessageEventAdapter extends TypeAdapter { + @override + final int typeId = 10; + + @override + EditMessageEvent read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return EditMessageEvent( + fields[0] as dynamic, + ); + } + + @override + void write(BinaryWriter writer, EditMessageEvent obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.payload); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is EditMessageEventAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/features/chat/providers/chat_provider.dart b/lib/features/chat/providers/chat_provider.dart new file mode 100644 index 00000000..2f8a1c0c --- /dev/null +++ b/lib/features/chat/providers/chat_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:telware_cross_platform/core/models/chat_model.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; + +final chatProvider = Provider.family((ref, chatId) { + return ref.watch(chatsViewModelProvider.notifier).getChatById(chatId); +}); diff --git a/lib/features/chat/repository/chat_local_repository.dart b/lib/features/chat/repository/chat_local_repository.dart index 3053db9f..7576b920 100644 --- a/lib/features/chat/repository/chat_local_repository.dart +++ b/lib/features/chat/repository/chat_local_repository.dart @@ -4,21 +4,22 @@ import 'package:flutter/foundation.dart'; import 'package:hive/hive.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/models/user_model.dart'; import 'package:telware_cross_platform/features/chat/models/message_event_models.dart'; class ChatLocalRepository { - final Box> _chatsBox; - final Box> _eventsBox; - final Box> _otherUsersBox; + final Box _chatsBox; + final Box _eventsBox; + final Box _otherUsersBox; static const String _eventsBoxKey = 'eventsQueue'; - static const String _chatsBoxKey = 'eventsQueue'; + static const String _chatsBoxKey = 'chatsList'; static const String _otherUsersBoxKey = 'otherUsers'; ChatLocalRepository({ - required Box> chatsBox, - required Box> eventsBox, - required Box> otherUsersBox, + required Box chatsBox, + required Box eventsBox, + required Box otherUsersBox, }) : _chatsBox = chatsBox, _eventsBox = eventsBox, _otherUsersBox = otherUsersBox; @@ -26,11 +27,13 @@ class ChatLocalRepository { ///////////////////////////////////// // set chats Future setChats(List list) async { + debugPrint('!!! set chats locally called'); try { await _chatsBox.put(_chatsBoxKey, list); return true; } catch (e) { debugPrint('!!! exception on saving the chats list'); + debugPrint(e.toString()); return false; } } @@ -38,25 +41,32 @@ class ChatLocalRepository { // get chats List getChats() { final list = _chatsBox.get(_chatsBoxKey, defaultValue: []); - return list ?? []; + final chats = + list?.map((element) => element as ChatModel).toList() ?? []; + return chats; } ///////////////////////////////////// // get other users Future setOtherUsers(Map otherUsers) async { try { - await _otherUsersBox.put(_otherUsersBox, otherUsers); + await _otherUsersBox.put(_otherUsersBoxKey, otherUsers); return true; } catch (e) { debugPrint('!!! exception on saving the other users list'); + debugPrint(e.toString()); return false; } } Map getOtherUsers() { - return _otherUsersBox - .get(_otherUsersBoxKey, defaultValue: {}) ?? - {}; + final map = _otherUsersBox + .get(_otherUsersBoxKey, defaultValue: {}); + final otherUsersMap = + map?.map((key, value) => MapEntry(key as String, value as UserModel)) ?? + {}; + + return otherUsersMap; } ///////////////////////////////////// @@ -76,7 +86,9 @@ class ChatLocalRepository { // get event queue Queue getEventQueue() { final list = _eventsBox.get(_eventsBoxKey, defaultValue: []); - final queue = Queue.from(list ?? []); + final eventsList = + list?.map((element) => element as MessageEvent).toList() ?? []; + final queue = Queue.from(eventsList); return queue; } } diff --git a/lib/features/chat/repository/chat_remote_repository.dart b/lib/features/chat/repository/chat_remote_repository.dart index c651cb7b..71892fec 100644 --- a/lib/features/chat/repository/chat_remote_repository.dart +++ b/lib/features/chat/repository/chat_remote_repository.dart @@ -41,4 +41,16 @@ class ChatRemoteRepository { return (appError: AppError('This User was not Found', code: 404), otherUser: null); } } + + Future<({ + AppError? appError, + ChatModel? chat + })> getChat(String sessionID, String chatID) async { + try { + return (appError: null, chat: null); + } catch (e) { + debugPrint('!!! Faild to get other user data, ${e.toString()}'); + return (appError: AppError('Chat was not found', code: 404), chat: null); + } + } } diff --git a/lib/features/chat/services/chat_mocking_service.dart b/lib/features/chat/services/chat_mocking_service.dart new file mode 100644 index 00000000..9d36c1d3 --- /dev/null +++ b/lib/features/chat/services/chat_mocking_service.dart @@ -0,0 +1,123 @@ +import 'dart:math'; + +import 'package:faker/faker.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/features/chat/enum/chatting_enums.dart'; + +import 'package:telware_cross_platform/features/chat/enum/message_enums.dart'; + +class ChatMockingService { + // Private constructor + ChatMockingService._internal(); + + // Singleton instance + static ChatMockingService? _instance; + + // Getter for the singleton instance + static ChatMockingService get instance { + return _instance ??= ChatMockingService._internal(); + } + + // Method to create a mocked user + UserModel createMockedUser() { + final faker = Faker(); + + return UserModel( + username: faker.internet.userName(), + screenFirstName: faker.person.firstName(), + screenLastName: faker.person.lastName(), + email: faker.internet.email(), + photo: faker.internet.httpsUrl(), + status: faker.lorem.sentence(), + bio: faker.lorem.sentences(3).join(' '), + maxFileSize: 10485760, // 10 MB + automaticDownloadEnable: faker.randomGenerator.boolean(), + lastSeenPrivacy: 'Everyone', + readReceiptsEnablePrivacy: faker.randomGenerator.boolean(), + storiesPrivacy: 'Everyone', + picturePrivacy: 'Everyone', + invitePermissionsPrivacy: 'Everyone', + phone: faker.phoneNumber.us(), + id: faker.guid.guid(), + ); + } + + // Method to create a list of mocked users + List createMockedUsers(int count) { + return List.generate(count, (_) => createMockedUser()); + } + + List createMockedMessages( + int count, String otherUserId, String appUserId) { + final faker = Faker(); + + return List.generate(count, (index) { + final senderId = + faker.randomGenerator.boolean() ? otherUserId : appUserId; + return MessageModel( + senderId: senderId, + content: faker.lorem.sentence(), + timestamp: DateTime.now().subtract(Duration(minutes: index)), + messageType: MessageType.normal, + userStates: { + otherUserId: _getRandomMessageState(), + appUserId: _getRandomMessageState(), + }, + ); + }); + } + + // Helper method to get a random message state + MessageState _getRandomMessageState() { + final Random random = Random(); + final int stateIndex = random.nextInt(3); // 0, 1, or 2 + switch (stateIndex) { + case 0: + return MessageState.sent; + case 1: + return MessageState.read; + default: + return MessageState.sent; + } + } + + ChatModel createMockedChat(List messages, List users) { + final faker = Faker(); + + return ChatModel( + id: faker.guid.guid(), + title: faker.lorem.words(3).join(' '), + userIds: users, + type: ChatType.private, + messages: messages, + description: faker.lorem.sentence(), + lastMessageTimestamp: + messages.isNotEmpty ? messages.first.timestamp : null, + isArchived: faker.randomGenerator.boolean(), + isMuted: faker.randomGenerator.boolean(), + isMentioned: faker.randomGenerator.boolean(), + draft: faker.lorem.sentence(), + ); + } + + ({List chats, List users}) createMockedChats( + int count, + String appUserId, + ) { + final faker = Faker(); + + final users = createMockedUsers(count); + final chats = List.generate(count, (index) { + final msgs = createMockedMessages( + faker.randomGenerator.integer(25, min: 1), + users[index].id!, + appUserId, + ); + return createMockedChat(msgs, [users[index].id!, appUserId]); + }); + + return (chats: chats, users: users); + } +} diff --git a/lib/features/chat/view/screens/chat_screen.dart b/lib/features/chat/view/screens/chat_screen.dart index be5f59ec..bcaa8fe2 100644 --- a/lib/features/chat/view/screens/chat_screen.dart +++ b/lib/features/chat/view/screens/chat_screen.dart @@ -1,43 +1,45 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; // Import flutter_svg import 'package:intl/intl.dart'; -import 'package:lottie/lottie.dart'; -import 'package:telware_cross_platform/core/mock/messages_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/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/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/bottom_input_bar_widget.dart'; import 'package:telware_cross_platform/features/chat/view/widget/chat_header_widget.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'; -class ChatScreen extends StatefulWidget { +class ChatScreen extends ConsumerStatefulWidget { static const String route = '/chat'; - final ChatModel chatModel; + final String chatId; - const ChatScreen({super.key, required this.chatModel}); + const ChatScreen({super.key, required this.chatId}); @override - State createState() => _ChatScreen(); + ConsumerState createState() => _ChatScreen(); } -class _ChatScreen extends State with WidgetsBindingObserver { +class _ChatScreen extends ConsumerState with WidgetsBindingObserver { List chatContent = []; final TextEditingController _controller = TextEditingController(); final ScrollController _scrollController = ScrollController(); - late ChatType type; + late ChatModel? chatModel; @override void initState() { super.initState(); - _controller.text = widget.chatModel.draft ?? ""; - type = widget.chatModel.type; WidgetsBinding.instance.addObserver(this); - final messages = widget.chatModel.id != null ? generateFakeMessages() : []; - chatContent = _generateChatContentWithDateLabels(messages); + WidgetsBinding.instance.addPostFrameCallback((_) { + chatModel = ref.read(chatProvider(widget.chatId)); + _controller.text = chatModel!.draft ?? ""; + }); _scrollToBottom(); } @@ -49,11 +51,14 @@ class _ChatScreen extends State with WidgetsBindingObserver { super.dispose(); } - List _generateChatContentWithDateLabels(List messages) { + List _generateChatContentWithDateLabels( + List messages) { List chatContent = []; for (int i = 0; i < messages.length; i++) { - if (i == 0 || !isSameDay(messages[i - 1].timestamp, messages[i].timestamp)) { - chatContent.add(DateLabelWidget(label: DateFormat('MMMM d').format(messages[i].timestamp))); + if (i == 0 || + !isSameDay(messages[i - 1].timestamp, messages[i].timestamp)) { + chatContent.add(DateLabelWidget( + label: DateFormat('MMMM d').format(messages[i].timestamp))); } chatContent.add(messages[i]); } @@ -61,7 +66,9 @@ class _ChatScreen extends State with WidgetsBindingObserver { } bool isSameDay(DateTime date1, DateTime date2) { - return date1.year == date2.year && date1.month == date2.month && date1.day == date2.day; + return date1.year == date2.year && + date1.month == date2.month && + date1.day == date2.day; } // Called when the keyboard visibility changes @@ -76,7 +83,8 @@ class _ChatScreen extends State with WidgetsBindingObserver { // Check if the user is already at the bottom of the scroll view bool _isAtBottom() { - if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { return true; } return false; @@ -84,7 +92,7 @@ class _ChatScreen extends State with WidgetsBindingObserver { // Scroll the chat view to the bottom void _scrollToBottom() { - Future.delayed(Duration(milliseconds: 100), () { + Future.delayed(const Duration(milliseconds: 100), () { if (_scrollController.hasClients) { _scrollController.jumpTo(_scrollController.position.maxScrollExtent); } @@ -93,13 +101,19 @@ class _ChatScreen extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { - final String title = widget.chatModel.title; - final membersNumber = widget.chatModel.userIds.length; - final String subtitle = widget.chatModel.type == ChatType.oneToOne + final chatModel = ref.watch(chatProvider(widget.chatId))!; + + final type = chatModel.type; + final String title = chatModel.title; + final membersNumber = chatModel.userIds.length; + final String subtitle = chatModel.type == ChatType.private ? "last seen a long time ago" : "$membersNumber Member${membersNumber > 1 ? "s" : ""}"; - final imageBytes = widget.chatModel.photoBytes; - final photo = widget.chatModel.photo; + final imageBytes = chatModel.photoBytes; + final photo = chatModel.photo; + final messages = + chatModel.id != null ? chatModel.messages : []; + chatContent = _generateChatContentWithDateLabels(messages); return Scaffold( appBar: AppBar( @@ -116,9 +130,7 @@ class _ChatScreen extends State with WidgetsBindingObserver { imageBytes: imageBytes, ), actions: [ - IconButton( - onPressed: () {}, - icon: const Icon(Icons.more_vert)) + IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)) ], ), body: GestureDetector( @@ -142,79 +154,81 @@ class _ChatScreen extends State with WidgetsBindingObserver { Column( children: [ Expanded( - child: 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, + child: 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), + ), + ], ), - const SizedBox(height: 10), - const Text( - 'Send a message or tap the greeting below.', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Palette.primaryText, - ), + 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: getRandomLottieAnimation(), + width: 100, + height: 100, + isLooping: true, + ), + ], ), - const SizedBox(height: 20), - LottieViewer( - path: getRandomLottieAnimation(), - 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.map((item) { + if (item is DateLabelWidget) { + return item; + } else if (item is MessageModel) { + return MessageTileWidget( + messageModel: item, + isSentByMe: item.senderId == ref.read(userProvider)!.id, + showInfo: type == ChatType.group, + ); + } else { + return const SizedBox.shrink(); + } + }).toList(), ), - ], + ), ), - ), - ) - : - SingleChildScrollView( - controller: _scrollController, // Use the ScrollController - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), - child: Column( - children: chatContent.map((item) { - if (item is DateLabelWidget) { - return item; - } else if (item is MessageModel) { - return MessageTileWidget( - messageModel: item, - isSentByMe: item.senderName == "John Doe", - showInfo: type == ChatType.group, - ); - } else { - return const SizedBox.shrink(); - } - }).toList(), - ), - ), - ), ), // The input bar at the bottom BottomInputBarWidget(controller: _controller), @@ -270,7 +284,8 @@ class _ChatScreen extends State with WidgetsBindingObserver { // Generate a random index Random random = Random(); - int randomIndex = random.nextInt(lottieAnimations.length); // Gets a random index + int randomIndex = + random.nextInt(lottieAnimations.length); // Gets a random index // Return the randomly chosen Lottie animation path return lottieAnimations[randomIndex]; diff --git a/lib/features/chat/view/screens/create_chat_screen.dart b/lib/features/chat/view/screens/create_chat_screen.dart index 3e2a3a75..5c00f9e3 100644 --- a/lib/features/chat/view/screens/create_chat_screen.dart +++ b/lib/features/chat/view/screens/create_chat_screen.dart @@ -13,6 +13,7 @@ import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/theme/sizes.dart'; import 'package:telware_cross_platform/core/view/widget/lottie_viewer.dart'; import 'package:telware_cross_platform/features/auth/view/widget/title_element.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/user/view/widget/user_chats.dart'; import 'chat_screen.dart'; @@ -26,7 +27,8 @@ class CreateChatScreen extends ConsumerStatefulWidget { ConsumerState createState() => _CreateChatScreen(); } -class _CreateChatScreen extends ConsumerState with TickerProviderStateMixin { +class _CreateChatScreen extends ConsumerState + with TickerProviderStateMixin { late List> fullUserChats; late List> userChats; late List> channelChats; @@ -40,15 +42,14 @@ class _CreateChatScreen extends ConsumerState with TickerProvi void initState() { super.initState(); _tabController = TabController(vsync: this, length: 9); - fullUserChats = >[{"options": >[]}]; - _usersFuture = generateFakeUsers( - count: 20, - photoPaths: [ - 'assets/imgs/marwan.jpg', - 'assets/imgs/ahmed.jpeg', - 'assets/imgs/bishoy.jpeg' - ] - ); + fullUserChats = >[ + {"options": >[]} + ]; + _usersFuture = generateFakeUsers(count: 20, photoPaths: [ + 'assets/imgs/marwan.jpg', + 'assets/imgs/ahmed.jpeg', + 'assets/imgs/bishoy.jpeg' + ]); } @override @@ -74,10 +75,9 @@ class _CreateChatScreen extends ConsumerState with TickerProvi decoration: const InputDecoration( hintText: 'Search', hintStyle: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - color: Palette.accentText - ), + fontSize: 18, + fontWeight: FontWeight.w400, + color: Palette.accentText), border: InputBorder.none, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, @@ -85,9 +85,9 @@ class _CreateChatScreen extends ConsumerState with TickerProvi ), cursorColor: Palette.accent, style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w400, - color: Palette.primaryText ), + fontSize: 18, + fontWeight: FontWeight.w400, + color: Palette.primaryText), onChanged: filterView, ), bottom: TabBar( @@ -115,7 +115,8 @@ class _CreateChatScreen extends ConsumerState with TickerProvi ), unselectedLabelColor: Palette.accentText, physics: const BouncingScrollPhysics(), - labelPadding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 2.0), + labelPadding: + const EdgeInsets.only(left: 18.0, right: 18.0, top: 2.0), tabs: const [ Tab(text: 'Chats'), Tab(text: 'Channels'), @@ -134,7 +135,8 @@ class _CreateChatScreen extends ConsumerState with TickerProvi builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( - child: CircularProgressIndicator(), // Show a loading indicator while data is loading + child: + CircularProgressIndicator(), // Show a loading indicator while data is loading ); } else if (snapshot.hasError) { return Center( @@ -211,9 +213,9 @@ class _CreateChatScreen extends ConsumerState with TickerProvi void _createNewChat(UserModel userInfo) { final myUser = ref.read(userProvider)!; ChatModel newChat = ChatModel( - title: userInfo.screenName, + title: '${userInfo.screenFirstName} ${userInfo.screenLastName}', userIds: [myUser.id!, userInfo.id!], - type: ChatType.oneToOne, + type: ChatType.private, messages: [], photo: userInfo.photo, ); @@ -230,11 +232,13 @@ class _CreateChatScreen extends ConsumerState with TickerProvi for (int i = 0; i < count; i++) { String username = faker.internet.userName(); - String screenName = faker.person.name(); + String screenFirstName = faker.person.firstName(); + String screenLastName = faker.person.lastName(); String email = faker.internet.email(); String status = faker.lorem.sentence(); String bio = faker.lorem.sentences(2).join(' '); - int maxFileSize = random.nextInt(50) * 1024 * 1024; // Random file size in MB + int maxFileSize = + random.nextInt(50) * 1024 * 1024; // Random file size in MB bool automaticDownloadEnable = random.nextBool(); String lastSeenPrivacy = randomPrivacy(); bool readReceiptsEnablePrivacy = random.nextBool(); @@ -254,7 +258,8 @@ class _CreateChatScreen extends ConsumerState with TickerProvi users.add(UserModel( username: username, - screenName: screenName, + screenFirstName: screenFirstName, + screenLastName: screenLastName, email: email, photo: photo, status: status, @@ -274,7 +279,7 @@ class _CreateChatScreen extends ConsumerState with TickerProvi for (UserModel user in users) { var option = { - "text": user.screenName, + "text": user.screenFirstName, "imagePath": user.photo, "subtext": "last seen Nov 23 at 6:40 PM", "trailingFontSize": 13.0, @@ -295,7 +300,10 @@ class _CreateChatScreen extends ConsumerState with TickerProvi } void filterView(String query) { - var filteredChats = >[{"options": >[]}];; + var filteredChats = >[ + {"options": >[]} + ]; + ; if (query.isEmpty) { filteredChats = List.from(fullUserChats); } else { 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 4d0c3b02..39d1e172 100644 --- a/lib/features/chat/view/widget/bottom_input_bar_widget.dart +++ b/lib/features/chat/view/widget/bottom_input_bar_widget.dart @@ -1,21 +1,24 @@ -import 'package:flutter/cupertino.dart'; 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/chatting_enums.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 BottomInputBarWidget extends StatelessWidget { +class BottomInputBarWidget extends ConsumerWidget { final TextEditingController controller; // Accept controller as a parameter const BottomInputBarWidget({super.key, required this.controller}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Container( - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), color: Palette.trinary, child: Row( children: [ IconButton( - icon: Icon(Icons.insert_emoticon), + icon: const Icon(Icons.insert_emoticon), color: Palette.accentText, onPressed: () {}, ), @@ -25,9 +28,7 @@ class BottomInputBarWidget extends StatelessWidget { decoration: const InputDecoration( hintText: 'Type a message', hintStyle: TextStyle( - color: Palette.accentText, - fontWeight: FontWeight.w400 - ), + color: Palette.accentText, fontWeight: FontWeight.w400), border: InputBorder.none, // No border focusedBorder: InputBorder.none, // No border when focused enabledBorder: InputBorder.none, // No border when enabled @@ -45,14 +46,14 @@ class BottomInputBarWidget extends StatelessWidget { children: [ if (text.text.isEmpty) ...[ IconButton( - icon: Icon(Icons.attach_file), + icon: const Icon(Icons.attach_file), color: Palette.accentText, onPressed: () { // Handle file attachment }, ), IconButton( - icon: Icon(Icons.mic), + icon: const Icon(Icons.mic), color: Palette.accentText, onPressed: () { // Handle mic @@ -60,7 +61,7 @@ class BottomInputBarWidget extends StatelessWidget { ), ] else IconButton( - icon: Icon(Icons.send), + icon: const Icon(Icons.send), color: Palette.accent, onPressed: () { // Handle send action @@ -74,4 +75,13 @@ class BottomInputBarWidget extends StatelessWidget { ), ); } + + void sendMsg(WidgetRef ref) { + ref.read(chattingControllerProvider).sendMsg( + content: controller.text, + msgType: MessageType.normal, + contentType: MessageContentType.text, + chatType: ChatType.private, + ); + } } diff --git a/lib/features/chat/view/widget/chat_tile_widget.dart b/lib/features/chat/view/widget/chat_tile_widget.dart index 4eb873f1..da1809d5 100644 --- a/lib/features/chat/view/widget/chat_tile_widget.dart +++ b/lib/features/chat/view/widget/chat_tile_widget.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.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/message_model.dart'; -import 'package:telware_cross_platform/core/models/user_model.dart'; +import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/utils.dart'; -import 'package:telware_cross_platform/features/chat/view/screens/chat_screen.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/user/view/widget/avatar_generator.dart'; class ChatTileWidget extends StatelessWidget { @@ -28,13 +28,13 @@ class ChatTileWidget extends StatelessWidget { final imageBytes = chatModel.photoBytes; final hasDraft = chatModel.draft?.isNotEmpty ?? false; final isGroupChat = chatModel.type == ChatType.group; - final messageStateIcon = sentByUser ? Icon(getMessageStateIcon(displayMessage), size: 16, color: Palette.accent) : null; + final messageStateIcon = sentByUser ? Icon(getMessageStateIcon(displayMessage), size: 16, color: Palette.accent,) : null; final unreadCount = _getUnreadMessageCount(); final isMuted = chatModel.isMuted; final isMentioned = chatModel.isMentioned; return InkWell( - onTap: () { context.push(ChatScreen.route, extra: chatModel); }, + onTap: () { context.push(Routes.chatScreen, extra: chatModel.id); }, child: Container( color: Palette.secondary, child: Row( @@ -118,7 +118,7 @@ class ChatTileWidget extends StatelessWidget { children: [ TextSpan( text: hasDraft ? "Draft: " - : isGroupChat ? "${displayMessage.senderName.split(" ")[0]}: " : "", + : isGroupChat ? "${'John Doe'.split(" ")[0]}: " : "", style: TextStyle( color: hasDraft ? Palette.error : isGroupChat ? Palette.primaryText : Palette.accentText, diff --git a/lib/features/chat/view/widget/message_tile_widget.dart b/lib/features/chat/view/widget/message_tile_widget.dart index fb709fc2..3abbf1ee 100644 --- a/lib/features/chat/view/widget/message_tile_widget.dart +++ b/lib/features/chat/view/widget/message_tile_widget.dart @@ -33,7 +33,7 @@ class MessageTileWidget extends StatelessWidget { IconData messageState = getMessageStateIcon(messageModel); Widget senderNameWidget = showInfo && !isSentByMe ? Text( - messageModel.senderName, + messageModel.senderId, style: TextStyle( fontWeight: FontWeight.bold, color: nameColor, diff --git a/lib/features/chat/view_model/chats_view_model.dart b/lib/features/chat/view_model/chats_view_model.dart index 0f2e4398..28f9b2d0 100644 --- a/lib/features/chat/view_model/chats_view_model.dart +++ b/lib/features/chat/view_model/chats_view_model.dart @@ -2,16 +2,18 @@ import 'package:riverpod_annotation/riverpod_annotation.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/features/chat/enum/message_enums.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; part 'chats_view_model.g.dart'; @Riverpod(keepAlive: true) class ChatsViewModel extends _$ChatsViewModel { - /// The state of the class, respembels a sorted list + /// The state of the class, respembels a sorted list /// of chats, based on the timesatmp of the latest msg /// in them - + Map _chatsMap = {}; Map _otherUsers = {}; @@ -26,11 +28,11 @@ class ChatsViewModel extends _$ChatsViewModel { Future getUser(String ID) async { var user = _otherUsers[ID]; - + if (user == null) { user = await ref.read(chattingControllerProvider).getOtherUser(ID); _otherUsers[ID] = user; - // todo: ask the controller to restore the other users map in the local storage + // todo: ask the controller to restore the other users map in the local storage ref.read(chattingControllerProvider).restoreOtherUsers(_otherUsers); } @@ -40,23 +42,44 @@ class ChatsViewModel extends _$ChatsViewModel { void setChats(List chats) { state = chats; _chatsMap.clear(); - _chatsMap = { for (var chat in chats) chat.id! : chat }; + _chatsMap = {for (var chat in chats) chat.id!: chat}; } - void addMessage(MessageModel msg, String chatID) { + void addSentMessage(String content, String chatID, MessageType msgType) { final chat = _chatsMap[chatID]; // todo(ahmed): make sure that new chats are added to the map first // I mean, new chats from the backend + + final MessageModel msg = MessageModel( + senderId: ref.read(userProvider)!.id!, + timestamp: DateTime.now(), + content: content, + messageType: msgType, + ); + chat!.messages.insert(0, msg); _moveChatToFront(chatID); } + Future addReceivedMessage(String chatID, MessageModel msg) async { + var chat = _chatsMap[chatID]; + + if (chat == null) { + chat = await ref.read(chattingControllerProvider).getChat(chatID); + _chatsMap[chatID] = chat; + state.insert(0, chat); + } + + chat.messages.insert(0, msg); + _moveChatToFront(chatID); + } + void deleteMessage(String msgID, String chatID) { final chat = _chatsMap[chatID]; // todo: check if msg is really deleted or just not showed // Find the msg with the specified ID final msgIndex = chat!.messages.indexWhere((msg) => msg.id == msgID); - + if (msgIndex != -1) { chat.messages.removeAt(msgIndex); } @@ -66,20 +89,24 @@ class ChatsViewModel extends _$ChatsViewModel { final chat = _chatsMap[chatID]; // Find the msg with the specified ID final msgIndex = chat!.messages.indexWhere((msg) => msg.id == msgID); - + if (msgIndex != -1) { chat.messages[msgIndex].copyWith(content: content); } } + ChatModel? getChatById(String chatId) { + return _chatsMap[chatId]; + } + void _moveChatToFront(String id) { // Find the chat with the specified ID final chatIndex = state.indexWhere((chat) => chat.id == id); - + if (chatIndex != -1) { // Remove the chat from its current position final chat = state.removeAt(chatIndex); - + // Insert the chat at the front of the list state.insert(0, chat); } diff --git a/lib/features/chat/view_model/chats_view_model.g.dart b/lib/features/chat/view_model/chats_view_model.g.dart index 00743d68..69e5b768 100644 --- a/lib/features/chat/view_model/chats_view_model.g.dart +++ b/lib/features/chat/view_model/chats_view_model.g.dart @@ -6,7 +6,7 @@ part of 'chats_view_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$chatsViewModelHash() => r'a74d3b2b9de8674bbbb7c8848a32c1ea684b041c'; +String _$chatsViewModelHash() => r'acea2e50009bd0f311e19f03a0cf1bcd4b7f837e'; /// See also [ChatsViewModel]. @ProviderFor(ChatsViewModel) diff --git a/lib/features/chat/view_model/chatting_controller.dart b/lib/features/chat/view_model/chatting_controller.dart index 5c1db7d7..92356f55 100644 --- a/lib/features/chat/view_model/chatting_controller.dart +++ b/lib/features/chat/view_model/chatting_controller.dart @@ -1,7 +1,11 @@ +import 'dart:collection'; + import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hive/hive.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:telware_cross_platform/core/mock/constants_mock.dart'; import 'package:telware_cross_platform/core/mock/user_mock.dart'; import 'package:telware_cross_platform/core/models/chat_model.dart'; import 'package:telware_cross_platform/core/models/message_model.dart'; @@ -9,11 +13,13 @@ 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/services/socket_service.dart'; -import 'package:telware_cross_platform/features/chat/models/enums.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/models/message_event_models.dart'; import 'package:telware_cross_platform/features/chat/repository/chat_local_repository.dart'; import 'package:telware_cross_platform/features/chat/repository/chat_remote_repository.dart'; import 'package:telware_cross_platform/core/constants/server_constants.dart'; +import 'package:telware_cross_platform/features/chat/services/chat_mocking_service.dart'; import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; import 'package:telware_cross_platform/features/chat/view_model/event_handler.dart'; @@ -26,9 +32,9 @@ ChattingController chattingController(Ref ref) { ); final localRepo = ChatLocalRepository( - chatsBox: Hive.box>('chats-box'), - eventsBox: Hive.box>('chatting-events-box'), - otherUsersBox: Hive.box>('other-users-box')); + chatsBox: Hive.box('chats-box'), + eventsBox: Hive.box('chatting-events-box'), + otherUsersBox: Hive.box('other-users-box')); return ChattingController( localRepository: localRepo, @@ -38,6 +44,7 @@ ChattingController chattingController(Ref ref) { } class ChattingController { + // todo: transform this into a NotifierProvider late final EventHandler _eventHandler; final ChatRemoteRepository _remoteRepository; final ChatLocalRepository _localRepository; @@ -55,12 +62,16 @@ class ChattingController { } Future getUserChats() async { + // todo: make use of the return of this _remoteRepository.getUserChats(_ref.read(tokenProvider)!); } Future init() async { + debugPrint('!!! tried to enter controller init'); + debugPrint('!!! enter controller init'); // get the chats and give it to the chats view model from local final chats = _localRepository.getChats(); + debugPrint('!!! chats list length ${chats.length}'); _ref.read(chatsViewModelProvider.notifier).setChats(chats); // get the users list from local @@ -69,68 +80,136 @@ class ChattingController { // get the events and give it to the handler from local final events = _localRepository.getEventQueue(); - _eventHandler.init(events); + final newEvents = events.map((e) => e.copyWith(controller: this)); + _eventHandler.init(Queue.from(newEvents.toList())); + + } + + Future newLoginInit() async { + debugPrint('!!! newLoginInit called'); + if (USE_MOCK_DATA) { + final mocker = ChatMockingService.instance; + final response = mocker.createMockedChats(20, _ref.read(tokenProvider)!); + // descinding sorting for the chats, based on last message + response.chats.sort( + (a, b) => b.messages[0].timestamp.compareTo( + a.messages[0].timestamp, + ), + ); + + final otherUsersMap = { + for (var user in response.users) user.id!: user + }; + + debugPrint((await _localRepository.setChats(response.chats)).toString()); + debugPrint((await _localRepository.setOtherUsers(otherUsersMap)).toString()); + debugPrint('!!! ended the newLoginInit mock'); + return; + } + + final response = + await _remoteRepository.getUserChats(_ref.read(tokenProvider)!); + + if (response.appError == null) { + // todo(ahmed): for the notifier provider, return a state of fail + } else { + // descinding sorting for the chats, based on last message + response.chats.sort( + (a, b) => b.messages[0].timestamp.compareTo( + a.messages[0].timestamp, + ), + ); + + final otherUsersMap = { + for (var user in response.users) user.id!: user + }; + + _localRepository.setChats(response.chats); + _localRepository.setOtherUsers(otherUsersMap); + debugPrint('!!! ended the newLoginInit'); + } + } /// Send a text message. /// Must specify either the chatID or userID in case of new chat, /// otherwise, it will throw an error - void sendMsg(String content, {String? chatID, String? userID}) { + void sendMsg({ + required String content, + required MessageType msgType, + required MessageContentType contentType, + required ChatType chatType, + String? chatID, + String? userID, + }) { // todo: handle new chats case if (chatID == null && userID == null) { throw Exception('specify the chatID, or userID in case of new chats'); } - final MessageModel msg = MessageModel( - senderName: _ref.read(userProvider)!.screenName, - timestamp: DateTime.now(), - content: content, - ); - - final msgEvent = SendMessageEvent(this, { + final msgEvent = SendMessageEvent({ 'chatId': chatID ?? userID, 'content': content, - 'contentType': 'text', + 'contentType': contentType.content, 'senderId': _ref.read(userProvider)!.id, - 'isFirstTime': chatID == null - }); + 'isFirstTime': chatID == null, + 'chatType': chatType.type + }, controller: this); _eventHandler.addEvent(msgEvent); // todo: handle new chats case ----------------------------------! - _ref.read(chatsViewModelProvider.notifier).addMessage(msg, chatID!); + _ref + .read(chatsViewModelProvider.notifier) + .addSentMessage(content, chatID!, msgType); + _localRepository.setChats(_ref.read(chatsViewModelProvider)); + } + + void receiveMsg(String chatID, MessageModel msg) { + _ref.read(chatsViewModelProvider.notifier).addReceivedMessage(chatID, msg); + _localRepository.setChats(_ref.read(chatsViewModelProvider)); } // delete a message - void deleteMsg(String msgID, String chatID, DeleteMsgType deleteType) { - final msgEvent = DeleteMessageEvent(this, { + void deleteMsg(String msgID, String chatID, DeleteMessageType deleteType) { + final msgEvent = DeleteMessageEvent({ 'chatId': chatID, 'senderId': _ref.read(userProvider)!.id, 'messageId': msgID, 'deleteType': deleteType.name - }); + }, controller: this); _eventHandler.addEvent(msgEvent); _ref.read(chatsViewModelProvider.notifier).deleteMessage(msgID, chatID); + _localRepository.setChats(_ref.read(chatsViewModelProvider)); } // edit a message void editMsg(String msgID, String chatID, String content) { - final msgEvent = DeleteMessageEvent(this, { + final msgEvent = DeleteMessageEvent({ 'chatId': chatID, 'senderId': _ref.read(userProvider)!.id, 'messageId': msgID, 'content': content - }); + }, controller: this); _eventHandler.addEvent(msgEvent); - _ref.read(chatsViewModelProvider.notifier).editMessage(msgID, chatID, content); + _ref + .read(chatsViewModelProvider.notifier) + .editMessage(msgID, chatID, content); + _localRepository.setChats(_ref.read(chatsViewModelProvider)); } // recieve a message + Future getChat(String chatID) async { + final sessionID = _ref.read(tokenProvider); + final response = await _remoteRepository.getChat(sessionID!, chatID); + return response.chat!; + } + Future getOtherUser(String id) async { // todo: call the remote repo and get the other user from server _remoteRepository.getOtherUser(id); diff --git a/lib/features/chat/view_model/event_handler.dart b/lib/features/chat/view_model/event_handler.dart index 83ec9d25..6b3d9abb 100644 --- a/lib/features/chat/view_model/event_handler.dart +++ b/lib/features/chat/view_model/event_handler.dart @@ -2,8 +2,9 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:telware_cross_platform/core/constants/server_constants.dart'; +import 'package:telware_cross_platform/core/models/message_model.dart'; import 'package:telware_cross_platform/core/services/socket_service.dart'; -import 'package:telware_cross_platform/features/chat/models/enums.dart'; +import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart'; import 'package:telware_cross_platform/features/chat/models/message_event_models.dart'; import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; @@ -17,7 +18,6 @@ class EventHandler { void init(Queue eventsQueue) { _queue = eventsQueue; - // todo(ahmed): check is this the right url _socket.connect(API_URL, _onSocketConnect); } @@ -34,7 +34,7 @@ class EventHandler { _stopRequested = true; // Gracefully request stopping the loop } - void _processQueue() async { + Future _processQueue() async { if (_isProcessing) return; // Avoid multiple loops _isProcessing = true; @@ -68,8 +68,13 @@ class EventHandler { void _onSocketConnect() { // receive a message - _socket.on(EventType.receiveMessage.event, (response) { - // send the message to the chatting controller + _socket.on(EventType.receiveMessage.event, (response) async { + try { + final msg = await MessageModel.fromMap(response); + _chattingController.receiveMsg(response['chatId'] ,msg); + } on Exception catch (e) { + debugPrint('!!! Error in recieving a message:\n${e.toString()}'); + } }); // todo(ahmed): add the rest of the recieved events @@ -93,7 +98,7 @@ class EventHandler { required ChattingController controller, required SocketService socket, }) { - _instance ??= + _instance = EventHandler._internal(controller: controller, socket: socket); } diff --git a/lib/features/home/view/screens/inbox_screen.dart b/lib/features/home/view/screens/inbox_screen.dart index 4ef6a058..a9907a0a 100644 --- a/lib/features/home/view/screens/inbox_screen.dart +++ b/lib/features/home/view/screens/inbox_screen.dart @@ -7,6 +7,7 @@ import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/palette.dart'; import 'package:telware_cross_platform/core/theme/sizes.dart'; import 'package:telware_cross_platform/features/chat/view/screens/create_chat_screen.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chatting_controller.dart'; import 'package:telware_cross_platform/features/home/view/widget/drawer.dart'; import 'package:telware_cross_platform/features/stories/view/widget/chats_list.dart'; import 'package:telware_cross_platform/features/stories/view/widget/colapsed_story_section.dart'; @@ -29,6 +30,9 @@ class _InboxScreenState extends ConsumerState { void initState() { super.initState(); _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(chattingControllerProvider).init(); + }); } void _scrollListener() { diff --git a/lib/features/home/view/widget/drawer.dart b/lib/features/home/view/widget/drawer.dart index 6fdcb0e4..dc579de5 100644 --- a/lib/features/home/view/widget/drawer.dart +++ b/lib/features/home/view/widget/drawer.dart @@ -52,7 +52,7 @@ class AppDrawer extends ConsumerWidget { userImageBytes == null ? Palette.primary : null, child: userImageBytes == null ? Text( - getInitials(user?.screenName ?? 'Moamen Hefny'), + getInitials('${user!.screenFirstName} ${user.screenLastName}'), style: const TextStyle( fontWeight: FontWeight.w500, color: Palette.primaryText, @@ -63,7 +63,7 @@ class AppDrawer extends ConsumerWidget { const SizedBox(height: 18), // todo: take real user name and number Text( - user?.screenName ?? 'Moamen Hefny', + '${user!.screenFirstName} ${user.screenLastName}', style: const TextStyle( color: Palette.primaryText, fontWeight: FontWeight.bold), ), diff --git a/lib/features/stories/view/widget/add_my_story.dart b/lib/features/stories/view/widget/add_my_story.dart index ed8dee7e..08ffc746 100644 --- a/lib/features/stories/view/widget/add_my_story.dart +++ b/lib/features/stories/view/widget/add_my_story.dart @@ -17,7 +17,7 @@ class AddMyStory extends StatelessWidget { @override Widget build(BuildContext context) { - debugPrint('Building AddMyStory...'); + // debugPrint('Building AddMyStory...'); return Column( children: [ Padding( diff --git a/lib/features/stories/view/widget/chats_list.dart b/lib/features/stories/view/widget/chats_list.dart index 5b52353d..3ea78c9b 100644 --- a/lib/features/stories/view/widget/chats_list.dart +++ b/lib/features/stories/view/widget/chats_list.dart @@ -1,99 +1,39 @@ -import 'dart:math'; -import 'package:faker/faker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/providers/user_provider.dart'; import 'package:telware_cross_platform/features/chat/view/widget/chat_tile_widget.dart'; +import 'package:telware_cross_platform/features/chat/view_model/chats_view_model.dart'; -class ChatsList extends StatelessWidget { +class ChatsList extends ConsumerWidget { const ChatsList({ super.key, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { debugPrint('Building chats_list...'); + final chatsList = ref.watch(chatsViewModelProvider); return SliverList( - delegate: SliverChildBuilderDelegate(_delegate, childCount: 20), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return _delegate( + chatsList[index], + ref.read(userProvider)!.id!, + ); + }, + childCount: chatsList.length, + ), ); } - Widget _delegate(BuildContext context, int index) { - final Faker faker = Faker(); - final Random random = Random(); - - // Randomly decide if a draft should exist - final hasDraft = random.nextBool(); - final draft = hasDraft ? faker.lorem.sentence() : null; - - // Random booleans for various chat properties - final isArchived = random.nextBool(); - final isMuted = random.nextBool(); - final isMentioned = random.nextBool(); - final isGroupChat = random.nextBool(); - - // Randomly generate message states for demonstration - final messageStates = _generateRandomMessageStates(); - final message = MessageModel( - senderName: faker.person.name(), - content: faker.lorem.sentence(), - timestamp: faker.date.dateTimeBetween( - DateTime.now().subtract(const Duration(days: 800)), // from one year ago - DateTime.now(), // to current date - ), - autoDeleteDuration: const Duration(hours: 1), // Example auto-delete duration - userStates: messageStates, - ); - final personName = faker.person.name(); + Widget _delegate(ChatModel chat, String userID) { + final message = chat.messages[0]; return ChatTileWidget( - chatModel: ChatModel( - id: faker.guid.guid(), - title: isGroupChat ? faker.company.name() : personName, - userIds: [faker.guid.guid()], - type: isGroupChat ? ChatType.group : ChatType.oneToOne, - description: faker.lorem.sentence(), - lastMessageTimestamp: DateTime.now(), - isArchived: isArchived, - isMuted: isMuted, - isMentioned: isMentioned, - draft: draft, - messages: [message], - ), + chatModel: chat, displayMessage: message, - sentByUser: message.senderName != personName, + sentByUser: message.senderId != userID, ); } - - // Helper method to generate random message states - Map _generateRandomMessageStates() { - final Faker faker = Faker(); - final Random random = Random(); - - // Generate 1 to 3 random message states - final int numStates = random.nextInt(3) + 1; - final Map messageStates = {}; - - for (int i = 0; i < numStates; i++) { - final String userId = faker.guid.guid(); - final MessageState state = _getRandomMessageState(); - messageStates[userId] = state; - } - - return messageStates; - } - - // Helper method to get a random message state - MessageState _getRandomMessageState() { - final Random random = Random(); - final int stateIndex = random.nextInt(3); // 0, 1, or 2 - switch (stateIndex) { - case 0: - return MessageState.sent; - case 1: - return MessageState.read; - default: - return MessageState.sent; - } - } } diff --git a/lib/features/stories/view/widget/expanded_stories_section.dart b/lib/features/stories/view/widget/expanded_stories_section.dart index 10e3a23e..081b2cb5 100644 --- a/lib/features/stories/view/widget/expanded_stories_section.dart +++ b/lib/features/stories/view/widget/expanded_stories_section.dart @@ -12,7 +12,7 @@ class ExpandedStoriesSection extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { ref.read(usersViewModelProvider.notifier).fetchContacts(); final isLoading = ref.watch(usersViewModelProvider.select((state) => state.isLoading)); - debugPrint('Building ExpandedStoriesSection...'); + // debugPrint('Building ExpandedStoriesSection...'); return LayoutBuilder( builder: (context, constraints) { diff --git a/lib/features/stories/view/widget/story_avatar.dart b/lib/features/stories/view/widget/story_avatar.dart index 4cf32d0e..ea69431c 100644 --- a/lib/features/stories/view/widget/story_avatar.dart +++ b/lib/features/stories/view/widget/story_avatar.dart @@ -19,7 +19,7 @@ class StoryAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - debugPrint('Building StoryAvatar...'); + // debugPrint('Building StoryAvatar...'); return GestureDetector( onTap: onTap, diff --git a/lib/features/stories/view/widget/story_with_user_name.dart b/lib/features/stories/view/widget/story_with_user_name.dart index 42a61107..49e4ca6b 100644 --- a/lib/features/stories/view/widget/story_with_user_name.dart +++ b/lib/features/stories/view/widget/story_with_user_name.dart @@ -15,7 +15,7 @@ class StoryWithUserName extends StatelessWidget { @override Widget build(BuildContext context) { - debugPrint('Building StoryWithUserName...'); + // debugPrint('Building StoryWithUserName...'); return Column( children: [ Padding( diff --git a/lib/features/user/repository/user_local_repository.dart b/lib/features/user/repository/user_local_repository.dart index a7a65bf0..c92cdeaa 100644 --- a/lib/features/user/repository/user_local_repository.dart +++ b/lib/features/user/repository/user_local_repository.dart @@ -57,7 +57,9 @@ class UserLocalRepository { try { final user = _userBox.get('user'); if (user != null) { - final updatedUser = user.copyWith(screenName: newScreenName); + final first = newScreenName.split(' ')[0]; + final last = newScreenName.split(' ')[1]; + final updatedUser = user.copyWith(screenFirstName: first, screenLastName: last); await _userBox.put('user', updatedUser); } else { return AppError("User not found."); diff --git a/lib/features/user/view/screens/profile_info_screen.dart b/lib/features/user/view/screens/profile_info_screen.dart index 10d15835..77e9670c 100644 --- a/lib/features/user/view/screens/profile_info_screen.dart +++ b/lib/features/user/view/screens/profile_info_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_shakemywidget/flutter_shakemywidget.dart'; import 'package:go_router/go_router.dart'; +import 'package:telware_cross_platform/core/models/user_model.dart'; import 'package:telware_cross_platform/core/theme/dimensions.dart'; import 'package:telware_cross_platform/features/user/repository/user_local_repository.dart'; import 'package:telware_cross_platform/features/user/view/widget/settings_input_widget.dart'; @@ -21,7 +22,7 @@ class ProfileInfoScreen extends ConsumerStatefulWidget { } class _ProfileInfoScreen extends ConsumerState with SingleTickerProviderStateMixin { - late final _user; + late final UserModel _user; final firstNameShakeKey = GlobalKey(); final lastNameShakeKey = GlobalKey(); @@ -36,8 +37,8 @@ class _ProfileInfoScreen extends ConsumerState with SingleTic super.initState(); // Initialize controllers based on userViewModelProvider data - _user = ref.read(userLocalRepositoryProvider).getUser(); - final nameParts = (_user?.screenName ?? '').split(' '); + _user = ref.read(userLocalRepositoryProvider).getUser()!; + final nameParts = [_user.screenFirstName, _user.screenLastName]; _firstNameController = TextEditingController(text: nameParts.isNotEmpty ? nameParts[0] : ''); _secondNameController = TextEditingController( text: nameParts.length > 1 ? nameParts.sublist(1).join(' ') : '', @@ -59,8 +60,7 @@ class _ProfileInfoScreen extends ConsumerState with SingleTic void _checkForChanges() { final screenName = '${_firstNameController.text} ${_secondNameController.text}'.trim(); - final hasChanged = _user != null && - (screenName != _user.screenName || _bioController.text != _user.bio); + final hasChanged = (screenName != '${_user.screenFirstName} ${_user.screenFirstName}' || _bioController.text != _user.bio); if (hasChanged != _showSaveButton) { setState(() { diff --git a/lib/features/user/view/widget/profile_header_widget.dart b/lib/features/user/view/widget/profile_header_widget.dart index c7d1d7a2..5131cca4 100644 --- a/lib/features/user/view/widget/profile_header_widget.dart +++ b/lib/features/user/view/widget/profile_header_widget.dart @@ -1,19 +1,8 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/features/auth/repository/auth_local_repository.dart'; -import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.dart'; -import 'package:telware_cross_platform/features/stories/repository/contacts_remote_repository.dart'; -import 'package:telware_cross_platform/features/stories/utils/utils_functions.dart'; -import 'package:telware_cross_platform/features/user/view_model/user_view_model.dart'; - -import '../../../../core/models/user_model.dart'; -import '../../../stories/models/contact_model.dart'; -import '../../../stories/view_model/contact_view_model.dart'; class ProfileHeader extends ConsumerWidget { @@ -39,7 +28,7 @@ class ProfileHeader extends ConsumerWidget { userImageBytes == null ? Palette.primary : null, child: userImageBytes == null ? Text( - getInitials(user?.screenName ?? 'Moamen Hefny'), + getInitials('${user!.screenFirstName} ${user.screenLastName}'), style: const TextStyle( fontWeight: FontWeight.w500, color: Palette.primaryText, @@ -53,7 +42,7 @@ class ProfileHeader extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - user?.screenName ?? 'Moamen Hefny', + '${user!.screenFirstName} ${user.screenLastName}', style: TextStyle( fontSize: 14 + 6 * factor / 100, fontWeight: FontWeight.bold, @@ -61,7 +50,7 @@ class ProfileHeader extends ConsumerWidget { ), ), Text( - user?.status ?? 'no status', + user.status, style: TextStyle( fontSize: 10 + 6 * factor / 100, color: Palette.accentText, diff --git a/lib/features/user/view_model/user_view_model.dart b/lib/features/user/view_model/user_view_model.dart index f8a8316c..f1612929 100644 --- a/lib/features/user/view_model/user_view_model.dart +++ b/lib/features/user/view_model/user_view_model.dart @@ -131,7 +131,7 @@ class UserViewModel extends _$UserViewModel { if (USE_MOCK_DATA) { // Simulate a successful update with mock data final updatedUser = - userMock.copyWith(screenName: "$firstName $lastName", bio: newBio); + userMock.copyWith(screenFirstName: firstName, screenLastName: lastName, bio: newBio); ref.read(authLocalRepositoryProvider).setUser(updatedUser); ref.read(userProvider.notifier).update((_) => updatedUser); state = UserState.success('Screen name updated successfully'); @@ -156,7 +156,7 @@ class UserViewModel extends _$UserViewModel { final user = ref.read(userProvider); if (user != null) { final updatedUser = - user.copyWith(screenName: "$firstName $lastName", bio: newBio); + user.copyWith(screenFirstName: firstName, screenLastName: lastName, bio: newBio); ref.read(authLocalRepositoryProvider).setUser(updatedUser); ref.read(userProvider.notifier).update((_) => updatedUser); } diff --git a/lib/features/user/view_model/user_view_model.g.dart b/lib/features/user/view_model/user_view_model.g.dart index ab1c0631..50d1ab2b 100644 --- a/lib/features/user/view_model/user_view_model.g.dart +++ b/lib/features/user/view_model/user_view_model.g.dart @@ -6,7 +6,7 @@ part of 'user_view_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$userViewModelHash() => r'afa2153e6bd36511f217c551843b9ab6432aad72'; +String _$userViewModelHash() => r'e98958ca37dea282302959e180ddccc72398b724'; /// See also [UserViewModel]. @ProviderFor(UserViewModel) diff --git a/lib/main.dart b/lib/main.dart index 9a515dab..83823381 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,9 @@ import 'package:telware_cross_platform/core/models/user_model.dart'; import 'package:telware_cross_platform/core/routes/routes.dart'; import 'package:telware_cross_platform/core/theme/app_theme.dart'; import 'package:telware_cross_platform/features/auth/view_model/auth_view_model.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/models/message_event_models.dart'; import 'package:telware_cross_platform/features/stories/models/contact_model.dart'; import 'package:telware_cross_platform/features/stories/models/story_model.dart'; @@ -26,11 +29,22 @@ Future init() async { Hive.registerAdapter(ContactModelBlockAdapter()); Hive.registerAdapter(ChatModelAdapter()); Hive.registerAdapter(MessageModelAdapter()); + Hive.registerAdapter(MessageEventAdapter()); + Hive.registerAdapter(SendMessageEventAdapter()); + Hive.registerAdapter(DeleteMessageEventAdapter()); + Hive.registerAdapter(EditMessageEventAdapter()); + Hive.registerAdapter(ChatTypeAdapter()); + Hive.registerAdapter(MessageStateAdapter()); + Hive.registerAdapter(MessageTypeAdapter()); + await Hive.initFlutter(); await Hive.openBox('contacts'); await Hive.openBox('contacts-block'); await Hive.openBox('auth-token'); await Hive.openBox('auth-user'); + await Hive.openBox('chats-box'); // List + await Hive.openBox('chatting-events-box'); // List + await Hive.openBox('other-users-box'); // Map await dotenv.load(fileName: "lib/.env"); } diff --git a/test/features/user/repositories/user_local_repository_test.dart b/test/features/user/repositories/user_local_repository_test.dart index 4ad6b8b5..8fd0f230 100644 --- a/test/features/user/repositories/user_local_repository_test.dart +++ b/test/features/user/repositories/user_local_repository_test.dart @@ -112,12 +112,12 @@ void main() { when(mockUserBox.put('user', any)).thenAnswer((_) async {}); final result = - await userLocalRepository.updateScreenName('New screen name'); + await userLocalRepository.updateScreenName('New name'); expect(result, isNull); verify(mockUserBox.get('user')).called(1); verify(mockUserBox.put( - 'user', user.copyWith(screenName: 'New screen name'))) + 'user', user.copyWith(screenFirstName: 'New', screenLastName: 'name'))) .called(1); }); @@ -138,13 +138,13 @@ void main() { when(mockUserBox.put('user', any)).thenThrow(Exception()); final result = - await userLocalRepository.updateScreenName('New screen name'); + await userLocalRepository.updateScreenName('New name'); expect((result as AppError).error, "Couldn't update screen name. Try again later."); verify(mockUserBox.get('user')).called(1); verify(mockUserBox.put( - 'user', user.copyWith(screenName: 'New screen name'))) + 'user', user.copyWith(screenFirstName: 'New', screenLastName: 'name'))) .called(1); });