ios_app_v2/yobble/Services/IncomingMessageCenter.swift
2025-10-21 20:42:36 +03:00

131 lines
4.3 KiB
Swift

import Foundation
import Combine
final class IncomingMessageCenter: ObservableObject {
@Published private(set) var banner: IncomingMessageBanner?
@Published var presentedChat: PrivateChatListItem?
@Published var pendingNavigation: ChatNavigationTarget?
var currentUserId: String?
var activeChatId: String?
private var dismissWorkItem: DispatchWorkItem?
private var cancellables = Set<AnyCancellable>()
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 }
activeChatId = banner.message.chatId
let chatItem = makeChatItem(from: banner.message)
if AppConfig.PRESENT_CHAT_AS_SHEET {
presentedChat = chatItem
pendingNavigation = nil
} else {
pendingNavigation = ChatNavigationTarget(chat: chatItem)
presentedChat = nil
}
dismissBanner()
}
private func handleIncoming(_ message: MessageItem) {
if let currentUserId, message.senderId == currentUserId {
return
}
if let activeChatId, activeChatId == message.chatId {
return
}
if AppConfig.PRESENT_CHAT_AS_SHEET,
let presentedChat,
presentedChat.chatId == message.chatId {
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)
}
struct ChatNavigationTarget: Identifiable {
let id = UUID()
let chat: PrivateChatListItem
}
}