feat(video_calls): incommig call handling fix for ios and anroid

This commit is contained in:
misha
2026-10-05 13:12:55 +03:00
parent 2dce28b196
commit df634fedd8
12 changed files with 1079 additions and 325 deletions

View File

@@ -5,9 +5,14 @@ import 'dart:developer';
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_chat_signal.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_ended_push_handler.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_incoming_push_presenter.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_push_payload.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/core/error_reporting/report_from.dart';
@@ -18,13 +23,22 @@ class ChatsCubit extends Cubit<ChatsState> {
StreamSubscription? newIncomingMessagesSubscription;
StreamSubscription? newLastMessageSubscription;
StreamSubscription<void>? reconnectedSubscription;
final Set<int> _presentedVideoCallMessageIds = {};
/// Test seam. Production uses CallKit.
Future<void> Function(VideoCallPushPayload payload)? presentIncomingVideoCall;
Future<void> Function()? dismissIncomingVideoCall;
ChatsCubit({
required this.chatRepository,
required this.wsManager,
required this.profileRepository,
this.presentIncomingVideoCall,
this.dismissIncomingVideoCall,
}) : super(ChatsState()) {
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) {
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((
message,
) {
_setNewLastMessage(message);
});
}
@@ -35,7 +49,9 @@ class ChatsCubit extends Cubit<ChatsState> {
Future<void> connectToWebSocket() async {
try {
await newIncomingMessagesSubscription?.cancel();
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((
event,
) {
_handleNewMessageViaSocket(event.data);
});
@@ -53,7 +69,9 @@ class ChatsCubit extends Cubit<ChatsState> {
_query = query;
emit(state.copyWith(state: BaseState.loading));
try {
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
final chatRoomsPaginationModel = await chatRepository.getChatRooms(
query: query,
);
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
emit(
@@ -66,9 +84,7 @@ class ChatsCubit extends Cubit<ChatsState> {
page++;
} catch (e, st) {
await reportFrom(e, st, this);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
);
emit(state.copyWith(errorMessage: e.toString(), state: BaseState.error));
}
}
@@ -76,7 +92,9 @@ class ChatsCubit extends Cubit<ChatsState> {
page = 1;
_query = query;
try {
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: query);
final chatRoomsPaginationModel = await chatRepository.getChatRooms(
query: query,
);
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
lastPage = chatRoomsPaginationModel.meta.lastPage + 1;
emit(
@@ -90,9 +108,7 @@ class ChatsCubit extends Cubit<ChatsState> {
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString(), stackTrace: st);
emit(
state.copyWith(errorMessage: e.toString(), state: BaseState.error),
);
emit(state.copyWith(errorMessage: e.toString(), state: BaseState.error));
}
}
@@ -100,7 +116,10 @@ class ChatsCubit extends Cubit<ChatsState> {
try {
if (state.isLoadingMoreChats || lastPage == page) return;
emit(state.copyWith(isLoadingMoreChats: true));
final chatRoomsPaginationModel = await chatRepository.getChatRooms(query: _query, page: page);
final chatRoomsPaginationModel = await chatRepository.getChatRooms(
query: _query,
page: page,
);
final unreadChats = chatRoomsPaginationModel.unreadChatsCount;
emit(
state.copyWith(
@@ -140,7 +159,9 @@ class ChatsCubit extends Cubit<ChatsState> {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: isChatRead ? state.chatsWithUnreadMessagesCount - 1 : state.chatsWithUnreadMessagesCount,
chatsWithUnreadMessagesCount: isChatRead
? state.chatsWithUnreadMessagesCount - 1
: state.chatsWithUnreadMessagesCount,
),
);
}
@@ -162,7 +183,8 @@ class ChatsCubit extends Cubit<ChatsState> {
wasChanged = true;
// Идемпотентность: повторная доставка того же message.id (реконнект,
// доставка по нескольким каналам) не начисляет непрочитанное дважды.
final isDuplicate = message.id != 0 && chatRoom.lastMessage?.id == message.id;
final isDuplicate =
message.id != 0 && chatRoom.lastMessage?.id == message.id;
// Собственное сообщение (второе устройство автора) обновляет
// lastMessage, но не создаёт непрочитанное (S3).
if (!isOwn && !isDuplicate && chatRoom.unreadMessagesCount == 0) {
@@ -170,7 +192,9 @@ class ChatsCubit extends Cubit<ChatsState> {
}
return chatRoom.copyWith(
lastMessage: message,
unreadMessagesCount: isOwn || isDuplicate ? chatRoom.unreadMessagesCount : chatRoom.unreadMessagesCount + 1,
unreadMessagesCount: isOwn || isDuplicate
? chatRoom.unreadMessagesCount
: chatRoom.unreadMessagesCount + 1,
);
}
return chatRoom;
@@ -182,15 +206,83 @@ class ChatsCubit extends Cubit<ChatsState> {
emit(
state.copyWith(
chatRooms: updatedChatRooms,
chatsWithUnreadMessagesCount: willIncrementUnreadChats ? state.chatsWithUnreadMessagesCount + 1 : state.chatsWithUnreadMessagesCount,
chatsWithUnreadMessagesCount: willIncrementUnreadChats
? state.chatsWithUnreadMessagesCount + 1
: state.chatsWithUnreadMessagesCount,
),
);
}
_presentVideoCallFromMessage(
message,
isOwn: isOwn,
chatRooms: currentChatRooms,
);
} catch (e, st) {
await reportFrom(e, st, this);
}
}
void _presentVideoCallFromMessage(
ChatMessageModel message, {
required bool isOwn,
required List<ChatRoom> chatRooms,
}) {
final kind = VideoCallChatSignal.kindOf(message);
if (kind == VideoCallChatSignalKind.ended) {
unawaited(_dismissIncomingVideoCallUi());
return;
}
if (isOwn || kind != VideoCallChatSignalKind.started) return;
if (message.id != 0 && !_presentedVideoCallMessageIds.add(message.id)) {
return;
}
final caller = _callerFromRooms(message, chatRooms);
final payload = VideoCallChatSignal.incomingPayload(
message: message,
callerName: caller?.name,
callerAvatarUrl: caller?.profilePhotoUrl,
);
if (payload == null) return;
unawaited(_showIncomingVideoCall(payload));
}
Future<void> _showIncomingVideoCall(VideoCallPushPayload payload) async {
try {
await (presentIncomingVideoCall ??
VideoCallIncomingPushPresenter.showIncomingCall)(payload);
} catch (e, st) {
log('ChatsCubit: failed to show incoming video call: $e', stackTrace: st);
}
}
Future<void> _dismissIncomingVideoCallUi() async {
try {
await (dismissIncomingVideoCall ??
VideoCallEndedPushHandler.dismissIncomingCallUi)();
} catch (e, st) {
log(
'ChatsCubit: failed to dismiss incoming video call: $e',
stackTrace: st,
);
}
}
ChatUser? _callerFromRooms(
ChatMessageModel message,
List<ChatRoom> chatRooms,
) {
for (final room in chatRooms) {
if (room.id != message.roomId) continue;
final from = room.users.fromChatUser;
final to = room.users.toChatUser;
if (from.id == message.authorId) return from;
if (to.id == message.authorId) return to;
}
return null;
}
void _setNewLastMessage(ChatMessageModel message) {
final chats = state.chatRooms.map((chat) {
if (chat.id == message.roomId) {