forked from eatvio/eatvio_app
refactor(chat): dedup
This commit is contained in:
@@ -252,22 +252,36 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
|
||||
/// Добавляет входящее сообщение в текущий список, не затирая более новый state.
|
||||
///
|
||||
/// Собственное сообщение, пришедшее по сокету, — это эхо с другого устройства
|
||||
/// автора (`author_id == wsManager.userId`). Такое сообщение тоже добавляется
|
||||
/// в открытый чат, но не считается входящим: звук не играет, а список чатов
|
||||
/// обновляет S3 через `new-chat-messages.{senderId}`.
|
||||
///
|
||||
/// Возвращает `true`, только если нужно проиграть звук входящего (чужое
|
||||
/// сообщение, реально добавленное).
|
||||
bool addIncomingMessage(ChatMessageModel message) {
|
||||
final currentUserId = wsManager.userId;
|
||||
if (currentUserId != null && message.authorId == currentUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final currentState = state;
|
||||
if (currentState is! ChatRoomStateLoaded) {
|
||||
return false;
|
||||
}
|
||||
if (currentState.chatMessages.any((existing) => existing.id == message.id && message.id != 0)) {
|
||||
|
||||
final currentUserId = wsManager.userId;
|
||||
final isOwn = currentUserId != null && message.authorId == currentUserId;
|
||||
|
||||
// Дедуп: по серверному id и по uuid (если broadcast вернул наш uuid, т.е.
|
||||
// это эхо уже добавленного оптимистичного сообщения).
|
||||
final alreadyExists = currentState.chatMessages.any(
|
||||
(existing) =>
|
||||
(message.id != 0 && existing.id == message.id) ||
|
||||
(message.uuid != null && existing.uuid == message.uuid),
|
||||
);
|
||||
if (alreadyExists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final added = _emitLoaded((current) {
|
||||
if (current.chatMessages.any((existing) => existing.id == message.id && message.id != 0)) {
|
||||
if (current.chatMessages.any((existing) => message.id != 0 && existing.id == message.id)) {
|
||||
return current;
|
||||
}
|
||||
return current.copyWith(
|
||||
@@ -278,8 +292,13 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
if (!added) {
|
||||
return false;
|
||||
}
|
||||
chatRepository.emitNewMessageToChats(message);
|
||||
return true;
|
||||
|
||||
// Список чатов за собственные сообщения обновляет S3 (new-chat-messages.{senderId}).
|
||||
if (!isOwn) {
|
||||
chatRepository.emitNewMessageToChats(message);
|
||||
}
|
||||
|
||||
return !isOwn;
|
||||
}
|
||||
|
||||
Future<void> _subscribeToPresenceChannel({required String chatId}) async {
|
||||
|
||||
@@ -1469,11 +1469,13 @@ void main() {
|
||||
roomId: 'chat-id',
|
||||
);
|
||||
|
||||
chatRoomCubit.addIncomingMessage(incoming);
|
||||
final shouldPlayIncomingSound = chatRoomCubit.addIncomingMessage(incoming);
|
||||
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(shouldPlayIncomingSound, isTrue);
|
||||
expect(state.chatMessages.first.id, 99);
|
||||
expect(state.chatMessages.length, 2);
|
||||
verify(mockChatRepository.emitNewMessageToChats(incoming)).called(1);
|
||||
});
|
||||
|
||||
test('addIncomingMessage - не добавляет дубликат по id', () async {
|
||||
@@ -1501,14 +1503,63 @@ void main() {
|
||||
roomId: 'chat-id',
|
||||
);
|
||||
|
||||
chatRoomCubit.addIncomingMessage(incoming);
|
||||
final shouldPlayIncomingSound = chatRoomCubit.addIncomingMessage(incoming);
|
||||
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(shouldPlayIncomingSound, isFalse);
|
||||
expect(state.chatMessages.length, 1);
|
||||
expect(state.chatMessages.first.data, 'Test message');
|
||||
verifyNever(mockChatRepository.emitNewMessageToChats(any));
|
||||
});
|
||||
|
||||
test('addIncomingMessage - игнорирует собственные сообщения', () async {
|
||||
test('addIncomingMessage - не добавляет дубликат по uuid (эхо оптимистичного сообщения)', () async {
|
||||
const uuid = 'room-uuid';
|
||||
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
|
||||
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
||||
when(mockWsManager.client).thenReturn(null);
|
||||
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
|
||||
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
|
||||
when(mockWsManager.directChatStream).thenReturn(null);
|
||||
when(mockWsManager.clientTypingStream).thenReturn(null);
|
||||
when(mockChatRepository.getChosenPlan(id: anyNamed('id'))).thenAnswer((_) async => throw Exception('No plan'));
|
||||
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
chatRoomCubit.addIncomingMessage(
|
||||
ChatMessageModel(
|
||||
id: 501,
|
||||
data: 'Echo with client uuid',
|
||||
mediaData: null,
|
||||
authorId: 2,
|
||||
updatedAt: DateTime.now(),
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: false,
|
||||
roomId: 'chat-id',
|
||||
uuid: 'client-uuid-1',
|
||||
),
|
||||
);
|
||||
|
||||
final shouldPlayIncomingSound = chatRoomCubit.addIncomingMessage(
|
||||
ChatMessageModel(
|
||||
id: 502,
|
||||
data: 'Same client uuid, server id differs',
|
||||
mediaData: null,
|
||||
authorId: 2,
|
||||
updatedAt: DateTime.now(),
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: false,
|
||||
roomId: 'chat-id',
|
||||
uuid: 'client-uuid-1',
|
||||
),
|
||||
);
|
||||
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(shouldPlayIncomingSound, isFalse);
|
||||
expect(state.chatMessages.where((message) => message.uuid == 'client-uuid-1').length, 1);
|
||||
});
|
||||
|
||||
test('addIncomingMessage - добавляет собственное сообщение (эхо второго устройства) без звука и без обновления списка', () async {
|
||||
const uuid = 'room-uuid';
|
||||
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => testChatRoom);
|
||||
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
|
||||
@@ -1523,7 +1574,7 @@ void main() {
|
||||
await chatRoomCubit.initChat(uuid);
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
chatRoomCubit.addIncomingMessage(
|
||||
final shouldPlayIncomingSound = chatRoomCubit.addIncomingMessage(
|
||||
ChatMessageModel(
|
||||
id: 77,
|
||||
data: 'Own echo',
|
||||
@@ -1531,14 +1582,36 @@ void main() {
|
||||
authorId: 1,
|
||||
updatedAt: DateTime.now(),
|
||||
isRead: true,
|
||||
isCurrentUserAuthor: true,
|
||||
isCurrentUserAuthor: false,
|
||||
roomId: 'chat-id',
|
||||
),
|
||||
);
|
||||
|
||||
final state = chatRoomCubit.state as ChatRoomStateLoaded;
|
||||
expect(state.chatMessages.length, 1);
|
||||
expect(state.chatMessages.any((message) => message.id == 77), isFalse);
|
||||
expect(shouldPlayIncomingSound, isFalse);
|
||||
expect(state.chatMessages.length, 2);
|
||||
expect(state.chatMessages.any((message) => message.id == 77), isTrue);
|
||||
// Список чатов за собственные сообщения обновляет S3 (new-chat-messages.{senderId}).
|
||||
verifyNever(mockChatRepository.emitNewMessageToChats(any));
|
||||
});
|
||||
|
||||
test('addIncomingMessage - игнорирует событие, когда state не loaded', () {
|
||||
final shouldPlayIncomingSound = chatRoomCubit.addIncomingMessage(
|
||||
ChatMessageModel(
|
||||
id: 42,
|
||||
data: 'Arrived before chat is loaded',
|
||||
mediaData: null,
|
||||
authorId: 2,
|
||||
updatedAt: DateTime.now(),
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: false,
|
||||
roomId: 'chat-id',
|
||||
),
|
||||
);
|
||||
|
||||
expect(shouldPlayIncomingSound, isFalse);
|
||||
expect(chatRoomCubit.state, isA<ChatRoomStateLoading>());
|
||||
verifyNever(mockChatRepository.emitNewMessageToChats(any));
|
||||
});
|
||||
|
||||
test('updateChosenPlanState - не затирает сообщения, пришедшие во время запроса', () async {
|
||||
|
||||
Reference in New Issue
Block a user