import Foundation import Combine final class IncomingMessageCenter: ObservableObject { @Published private(set) var banner: IncomingMessageBanner? @Published var presentedChat: PrivateChatListItem? var currentUserId: String? private var dismissWorkItem: DispatchWorkItem? private var cancellables = Set() private let notificationCenter: NotificationCenter init(notificationCenter: NotificationCenter = .default) { self.notificationCenter = notificationCenter notificationCenter.publisher(for: .socketDidReceivePrivateMessage) .compactMap { $0.object as? MessageItem } .receive(on: RunLoop.main) .sink { [weak self] message in self?.handleIncoming(message) } .store(in: &cancellables) } func dismissBanner() { dismissWorkItem?.cancel() dismissWorkItem = nil banner = nil } func openCurrentChat() { guard let banner else { return } presentedChat = makeChatItem(from: banner.message) dismissBanner() } private func handleIncoming(_ message: MessageItem) { if let currentUserId, message.senderId == currentUserId { return } banner = buildBanner(from: message) scheduleDismiss() } private func buildBanner(from message: MessageItem) -> IncomingMessageBanner { let sender = senderName(for: message) let preview = messagePreview(for: message) return IncomingMessageBanner(message: message, senderName: sender, messagePreview: preview) } private func senderName(for message: MessageItem) -> String { let candidates: [String?] = [ message.senderData?.customName, message.senderData?.fullName, message.senderData?.login.map { "@\($0)" } ] for candidate in candidates { if let value = candidate?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty { return value } } return message.senderId } private func messagePreview(for message: MessageItem) -> String { if let content = message.content?.trimmingCharacters(in: .whitespacesAndNewlines), !content.isEmpty { return content } switch message.messageType.lowercased() { case "image": return NSLocalizedString("Изображение", comment: "Image message placeholder") case "audio": return NSLocalizedString("Аудио", comment: "Audio message placeholder") case "video": return NSLocalizedString("Видео", comment: "Video message placeholder") default: return NSLocalizedString("Новое сообщение", comment: "Default banner subtitle") } } private func makeChatItem(from message: MessageItem) -> PrivateChatListItem { let profile = message.senderData return PrivateChatListItem( chatId: message.chatId, chatType: .privateChat, chatData: profile, lastMessage: message, createdAt: message.createdAt, unreadCount: 0 ) } private func scheduleDismiss(after delay: TimeInterval = 5) { dismissWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self] in self?.banner = nil self?.dismissWorkItem = nil } dismissWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem) } }