Skip to content

Commit

Permalink
feat(chat): add Pin, Reply, Edit, and Delete messages (#84)
Browse files Browse the repository at this point in the history
* feat: sockets auto reconnect v1

* fix: sockets auto reconnect v2

* undate

* fix: missing function call

* fix: messageing step 1

* fix: remove the chats map from the chatViewModel to insure consistency

* feature: add missing attributes

* feature: get missing attributes from the request

* Untrack the generated files

* refactor: add some organization to the chat screen

* feature: clear all chats and users data on log out

* fix: remove dublicate user name from msg

* feat: receive messages correctly

* feature: implement pin and reply

* fix: very tall reply message

* fix: dublicate recieved messages issue

* feature: add extra measures for establishing socket connection

* feat: delete a message

* feat: make an entire new socket connection

* feat: check delete msg success

* feature: handle keyboard dismiss in chat screen

* fix: having an expiered session

* feature: edit a message

* fix: complete merging
  • Loading branch information
Ahmed-Aladdiin authored Dec 13, 2024
1 parent 0cd7389 commit 1f4ed60
Show file tree
Hide file tree
Showing 22 changed files with 1,387 additions and 1,138 deletions.
61 changes: 39 additions & 22 deletions lib/core/models/message_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class MessageModel {
final bool isAnnouncement;
@HiveField(13)
List<String> threadMessages;
@HiveField(14)
final bool isEdited;

//<editor-fold desc="Data Methods">
MessageModel({
Expand All @@ -49,13 +51,14 @@ class MessageModel {
required this.timestamp,
this.id,
required this.userStates,
this.isPinned=false,
this.parentMessage,
this.localId = '',
this.isPinned = false,
this.isEdited = false,
this.isForward = false,
this.isAnnouncement = false,
List<String>? threadMessages,
}) : threadMessages = threadMessages ?? [];
}) : threadMessages = threadMessages ?? [];

void updateUserState(String userId, MessageState state) {
userStates[userId] = state;
Expand All @@ -71,6 +74,7 @@ class MessageModel {

return other.messageType == messageType &&
other.messageContentType == messageContentType &&
other.parentMessage == parentMessage &&
other.senderId == senderId &&
other.content == content &&
other.timestamp == timestamp &&
Expand All @@ -80,6 +84,7 @@ class MessageModel {
other.isAnnouncement == isAnnouncement &&
other.localId == localId &&
other.isForward == isForward &&
other.isEdited == isEdited &&
other.userStates == userStates;
}

Expand All @@ -93,6 +98,8 @@ class MessageModel {
timestamp.hashCode ^
id.hashCode ^
isAnnouncement.hashCode ^
parentMessage.hashCode ^
isEdited.hashCode ^
threadMessages.hashCode ^
isForward.hashCode ^
localId.hashCode ^
Expand All @@ -114,7 +121,9 @@ class MessageModel {
'isAnnouncement: $isAnnouncement\n'
'isForward: $isForward\n'
'isPinned: $isPinned\n'
'isEdited: $isEdited\n'
'threadMessages: $threadMessages'
'parentMessage: $parentMessage'
')');
}

Expand All @@ -133,6 +142,8 @@ class MessageModel {
bool? isForward,
bool? isPinned,
bool? isAnnouncement,
bool? isEdited,
String? parentMessage,
List<String>? threadMessages,
}) {
return MessageModel(
Expand All @@ -149,6 +160,8 @@ class MessageModel {
isPinned: isPinned ?? this.isPinned,
isAnnouncement: isAnnouncement ?? this.isAnnouncement,
threadMessages: threadMessages ?? this.threadMessages,
parentMessage: parentMessage ?? this.parentMessage,
isEdited: isEdited ?? this.isEdited,
);
}

Expand All @@ -168,37 +181,41 @@ class MessageModel {
'localId': localId,
'isForward': isForward,
'isPinned': isPinned,
'isEdited': isEdited,
'isAnnouncement': isAnnouncement,
'threadMessages': threadMessages,
'parentMessage': parentMessage,
};

return map;
}

static Future<MessageModel> fromMap(Map<String, dynamic> map) async {
return MessageModel(
senderId: map['senderId'] as String,
content: map['content'] as MessageContent?,
timestamp: DateTime.parse(map['timestamp'] as String),
id: map['messageId'] as String?,
messageType: MessageType.getType(map['messageType']),
messageContentType: MessageContentType.getType(map['messageContentType']),
autoDeleteTimestamp: map['autoDeleteTimeStamp'] != null
? DateTime.parse(map['autoDeleteTimeStamp'])
: null,
userStates: (map['userStates'] as Map<String, dynamic>?)!.map(
(key, value) => MapEntry(
key,
MessageState.values.firstWhere(
(e) => e.toString().split('.').last == value,
orElse: () => MessageState.sent,
senderId: map['senderId'] as String,
content: map['content'] as MessageContent?,
timestamp: DateTime.parse(map['timestamp'] as String),
id: map['messageId'] as String?,
messageType: MessageType.getType(map['messageType']),
messageContentType:
MessageContentType.getType(map['messageContentType']),
autoDeleteTimestamp: map['autoDeleteTimeStamp'] != null
? DateTime.parse(map['autoDeleteTimeStamp'])
: null,
userStates: (map['userStates'] as Map<String, dynamic>?)!.map(
(key, value) => MapEntry(
key,
MessageState.values.firstWhere(
(e) => e.toString().split('.').last == value,
orElse: () => MessageState.sent,
),
),
),
),
isForward: map['isForward'] ?? false,
isPinned: map['isPinned'] ?? false,
isAnnouncement: map['isAnnouncement'] ?? false,
);
isForward: map['isForward'] ?? false,
isPinned: map['isPinned'] ?? false,
isAnnouncement: map['isAnnouncement'] ?? false,
isEdited: map['isEdited'] ?? false,
parentMessage: map['parentMessageId'] ?? '');
}

String toJson({bool forSender = false}) =>
Expand Down
22 changes: 18 additions & 4 deletions lib/core/services/socket_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class SocketService {
late Function() _onConnect;
late EventHandler _eventHandler;
bool isConnected = false, _isReconnecting = false;
Timer? timer1, timer2;

final Duration _retryDelay =
Duration(seconds: SOCKET_RECONNECT_DELAY_SECONDS);
Expand All @@ -29,6 +30,11 @@ class SocketService {
_onConnect = onConnect ?? _onConnect;
_eventHandler = eventHandler ?? _eventHandler;

isConnected = false;
_isReconnecting = false;
timer1?.cancel();
timer2?.cancel();

_connect();
}

Expand Down Expand Up @@ -60,37 +66,45 @@ class SocketService {

_socket.onConnectError((error) {
debugPrint('### Connection error: $error');
isConnected = false;
onError();
});

_socket.onError((error) {
debugPrint('### Socket error: $error');
isConnected = false;
onError();
});

_socket.onDisconnect((_) {
debugPrint('Disconnected from server');
isConnected = false;
_isReconnecting = false;
});
}

void onError() {
_socket.disconnect();
isConnected = false;
if (isConnected) return;
disconnect();
_eventHandler.stopProcessing();

if (_isReconnecting) return;
_isReconnecting = true;
Timer(_retryDelay, () {
timer1 = Timer(_retryDelay, () {
_isReconnecting = false;
_connect();
timer2 = Timer(_retryDelay, () {
_isReconnecting = false;
onError();
});
});
}

void disconnect() {
debugPrint('*** called the socket disconnect');
_socket.disconnect();
_socket.destroy();
isConnected = false;
debugPrint('### Socket connection destroyed');
}

void on(String event, Function(dynamic data) callback) {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/auth/repository/auth_remote_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ class AuthRemoteRepository {
int? code;
if (dioException.response != null) {
code = dioException.response!.statusCode;
debugPrint(code.toString());
debugPrint('^&^ get me error with status: $code');
message =
dioException.response!.data?['message'] ?? 'Unexpected server Error';
debugPrint(message);
Expand Down
34 changes: 19 additions & 15 deletions lib/features/auth/view_model/auth_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class AuthViewModel extends _$AuthViewModel {

response.match((appError) {
state = AuthState.fail(appError.error);
if (appError.code == 401) {
_handleLogOutState();
return;
}
// getting user data from local as remote failed
final user = ref.read(authLocalRepositoryProvider).getMe();
ref.read(userProvider.notifier).update((_) => user);
Expand Down Expand Up @@ -294,7 +298,7 @@ class AuthViewModel extends _$AuthViewModel {
debugPrint('===============================');
debugPrint('log out operation ended');
debugPrint('Error: ${appError?.error}');
await _handleLogOutState(appError);
await _handleLogOutState();
}

Future<void> logOutAllOthers() async {
Expand All @@ -313,25 +317,21 @@ class AuthViewModel extends _$AuthViewModel {
state = AuthState.loading;
final token = ref.read(tokenProvider);

final appError = await ref
await ref
.read(authRemoteRepositoryProvider)
.logOut(token: token!, route: 'auth/logout-all');

await _handleLogOutState(appError);
await _handleLogOutState();
}

Future<void> _handleLogOutState(AppError? appError) async {
if (appError == null) {
// successful log out operation
await ref.read(authLocalRepositoryProvider).deleteToken();
ref.read(tokenProvider.notifier).update((_) => null);
Future<void> _handleLogOutState() async {
// successful log out operation
await ref.read(authLocalRepositoryProvider).deleteToken();
ref.read(tokenProvider.notifier).update((_) => null);

await ref.read(authLocalRepositoryProvider).deleteUser();
ref.read(userProvider.notifier).update((_) => null);
state = AuthState.unauthenticated;
} else {
state = AuthState.fail(appError.error);
}
await ref.read(authLocalRepositoryProvider).deleteUser();
ref.read(userProvider.notifier).update((_) => null);
state = AuthState.unauthenticated;
}

Future<void> getMe() async {
Expand All @@ -347,7 +347,11 @@ class AuthViewModel extends _$AuthViewModel {
// try getting updated user data
final response = await ref.read(authRemoteRepositoryProvider).getMe(token!);

response.match((appError) {}, (user) async {
response.match((appError) {
if (appError.code == 401) {
_handleLogOutState();
}
}, (user) async {
debugPrint('** getMe is called\nuser supposed to have img');
debugPrint(user.toString());
ref.read(authLocalRepositoryProvider).setUser(user);
Expand Down
41 changes: 41 additions & 0 deletions lib/features/chat/classes/message_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ abstract class MessageContent {
MessageContent copyWith();

String getContent();
String? getMediaURL();
}

// For Text Messages
Expand All @@ -37,6 +38,11 @@ class TextContent extends MessageContent {
String getContent() {
return text;
}

@override
String? getMediaURL() {
return null;
}
}

// For Audio Messages
Expand Down Expand Up @@ -81,6 +87,11 @@ class AudioContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return audioUrl;
}
}

// For Document Messages (PDFs, Docs)
Expand Down Expand Up @@ -119,6 +130,11 @@ class DocumentContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return fileUrl;
}
}

// For Image Messages
Expand Down Expand Up @@ -154,6 +170,11 @@ class ImageContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return imageUrl;
}
}

// For Video Messages
Expand Down Expand Up @@ -192,6 +213,11 @@ class VideoContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return videoUrl;
}
}

// for emoji, gifs and stickers
Expand Down Expand Up @@ -230,6 +256,11 @@ class EmojiContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return emojiUrl;
}
}

@HiveType(typeId: 21)
Expand Down Expand Up @@ -267,6 +298,11 @@ class GIFContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return gifUrl;
}
}

@HiveType(typeId: 22)
Expand Down Expand Up @@ -304,4 +340,9 @@ class StickerContent extends MessageContent {
String getContent() {
return '';
}

@override
String? getMediaURL() {
return stickerUrl;
}
}
Loading

0 comments on commit 1f4ed60

Please sign in to comment.