Групповые чаты: sender name/avatar в ячейках, multi-typer typing, фикс скачивания фото/аватарок и verified badge
This commit is contained in:
@@ -88,6 +88,7 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
private let signalPeerHandlersLock = NSLock()
|
||||
private let webRTCHandlersLock = NSLock()
|
||||
private let iceServersHandlersLock = NSLock()
|
||||
private let groupOneShotLock = NSLock()
|
||||
private let packetQueueLock = NSLock()
|
||||
private let searchRouter = SearchPacketRouter()
|
||||
private var resultHandlers: [UUID: (PacketResult) -> Void] = [:]
|
||||
@@ -95,6 +96,10 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
private var webRTCHandlers: [UUID: (PacketWebRTC) -> Void] = [:]
|
||||
private var iceServersHandlers: [UUID: (PacketIceServers) -> Void] = [:]
|
||||
|
||||
/// Generic one-shot handlers for group packets. Key: (packetId, handlerId).
|
||||
/// Handler returns `true` if it consumed the packet (auto-removed).
|
||||
private var groupOneShotHandlers: [UUID: (packetId: Int, handler: (any Packet) -> Bool)] = [:]
|
||||
|
||||
/// Background task to keep WebSocket alive during brief background periods (active call).
|
||||
/// iOS gives ~30s; enough for the call to survive app switching / notification interactions.
|
||||
private var callBackgroundTask: UIBackgroundTaskIdentifier = .invalid
|
||||
@@ -140,10 +145,13 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
return
|
||||
}
|
||||
case .connecting:
|
||||
if client.isConnecting {
|
||||
Self.logger.info("Connect already in progress, skipping duplicate connect()")
|
||||
return
|
||||
}
|
||||
// Always skip if already in connecting state. Previous code only
|
||||
// checked client.isConnecting which has a race gap — between
|
||||
// setting connectionState=.connecting and client.connect() setting
|
||||
// isConnecting=true, a second call would slip through.
|
||||
// This caused 3 parallel WebSocket connections (3x every packet).
|
||||
Self.logger.info("Connect already in progress, skipping duplicate connect()")
|
||||
return
|
||||
case .disconnected:
|
||||
break
|
||||
}
|
||||
@@ -189,13 +197,19 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
func forceReconnectOnForeground() {
|
||||
guard savedPublicKey != nil, savedPrivateHash != nil else { return }
|
||||
|
||||
// During an active call the WebSocket may still be alive (background task
|
||||
// keeps the process running for ~30s). Tearing it down would break signaling
|
||||
// and trigger server re-delivery of .call — causing endCallBecauseBusy.
|
||||
// If the connection is authenticated, trust it and skip reconnect.
|
||||
// During an active call, WebRTC media flows via DTLS/SRTP (not WebSocket).
|
||||
// Tearing down the socket would trigger server re-delivery of .call and
|
||||
// cause unnecessary signaling disruption (endCallBecauseBusy).
|
||||
// For .active phase: skip entirely — WS is only needed for endCall signal,
|
||||
// which will work after natural reconnect or ICE timeout ends the call.
|
||||
// For other call phases: skip only if WS is authenticated (still alive).
|
||||
if CallManager.shared.uiState.phase == .active {
|
||||
Self.logger.info("⚡ Foreground reconnect skipped — call active, media via DTLS")
|
||||
return
|
||||
}
|
||||
if CallManager.shared.uiState.phase != .idle,
|
||||
connectionState == .authenticated {
|
||||
Self.logger.info("⚡ Foreground reconnect skipped — active call, WS authenticated")
|
||||
Self.logger.info("⚡ Foreground reconnect skipped — call in progress, WS authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -205,7 +219,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
case .handshaking, .deviceVerificationRequired:
|
||||
return
|
||||
case .connecting:
|
||||
if client.isConnecting { return }
|
||||
// Same fix as connect() — unconditional return to prevent triple connections
|
||||
return
|
||||
case .authenticated, .connected, .disconnected:
|
||||
break // Always reconnect — .authenticated/.connected may be zombie on iOS
|
||||
}
|
||||
@@ -229,6 +244,10 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
func beginCallBackgroundTask() {
|
||||
guard callBackgroundTask == .invalid else { return }
|
||||
callBackgroundTask = UIApplication.shared.beginBackgroundTask(withName: "RosettaCall") { [weak self] in
|
||||
// Don't end the call here — CallKit keeps the process alive for active calls.
|
||||
// This background task only buys time for WebSocket reconnection.
|
||||
// Killing the call on expiry was causing premature call termination
|
||||
// during keyExchange phase (~30s before Desktop could respond).
|
||||
self?.endCallBackgroundTask()
|
||||
}
|
||||
Self.logger.info("📞 Background task started for call")
|
||||
@@ -252,8 +271,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
case .authenticated, .handshaking, .deviceVerificationRequired, .connected:
|
||||
return
|
||||
case .connecting:
|
||||
// Android parity: `(CONNECTING && isConnecting)` — skip if connect() is in progress.
|
||||
if client.isConnecting { return }
|
||||
// Unconditional return — prevent duplicate connections (same fix as connect())
|
||||
return
|
||||
case .disconnected:
|
||||
break
|
||||
}
|
||||
@@ -352,6 +371,8 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
var packet = PacketWebRTC()
|
||||
packet.signalType = signalType
|
||||
packet.sdpOrCandidate = sdpOrCandidate
|
||||
packet.publicKey = SessionManager.shared.currentPublicKey
|
||||
packet.deviceId = DeviceIdentityManager.shared.currentDeviceId()
|
||||
sendPacket(packet)
|
||||
}
|
||||
|
||||
@@ -452,6 +473,47 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
resultHandlersLock.unlock()
|
||||
}
|
||||
|
||||
// MARK: - Group One-Shot Handlers
|
||||
|
||||
/// Registers a one-shot handler for a specific packet type.
|
||||
/// Handler receives the packet and returns `true` to consume (auto-remove), `false` to keep.
|
||||
@discardableResult
|
||||
func addGroupOneShotHandler(packetId: Int, handler: @escaping (any Packet) -> Bool) -> UUID {
|
||||
let id = UUID()
|
||||
groupOneShotLock.lock()
|
||||
groupOneShotHandlers[id] = (packetId, handler)
|
||||
groupOneShotLock.unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
func removeGroupOneShotHandler(_ id: UUID) {
|
||||
groupOneShotLock.lock()
|
||||
groupOneShotHandlers.removeValue(forKey: id)
|
||||
groupOneShotLock.unlock()
|
||||
}
|
||||
|
||||
/// Called from `routeIncomingPacket` — dispatches to matching one-shot handlers.
|
||||
private func notifyGroupOneShotHandlers(packetId: Int, packet: any Packet) {
|
||||
groupOneShotLock.lock()
|
||||
let matching = groupOneShotHandlers.filter { $0.value.packetId == packetId }
|
||||
groupOneShotLock.unlock()
|
||||
|
||||
var consumed: [UUID] = []
|
||||
for (id, entry) in matching {
|
||||
if entry.handler(packet) {
|
||||
consumed.append(id)
|
||||
}
|
||||
}
|
||||
|
||||
if !consumed.isEmpty {
|
||||
groupOneShotLock.lock()
|
||||
for id in consumed {
|
||||
groupOneShotHandlers.removeValue(forKey: id)
|
||||
}
|
||||
groupOneShotLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Setup
|
||||
|
||||
private func setupClientCallbacks() {
|
||||
@@ -668,26 +730,32 @@ final class ProtocolManager: @unchecked Sendable {
|
||||
case 0x11:
|
||||
if let p = packet as? PacketCreateGroup {
|
||||
onCreateGroupReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x11, packet: p)
|
||||
}
|
||||
case 0x12:
|
||||
if let p = packet as? PacketGroupInfo {
|
||||
onGroupInfoReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x12, packet: p)
|
||||
}
|
||||
case 0x13:
|
||||
if let p = packet as? PacketGroupInviteInfo {
|
||||
onGroupInviteInfoReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x13, packet: p)
|
||||
}
|
||||
case 0x14:
|
||||
if let p = packet as? PacketGroupJoin {
|
||||
onGroupJoinReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x14, packet: p)
|
||||
}
|
||||
case 0x15:
|
||||
if let p = packet as? PacketGroupLeave {
|
||||
onGroupLeaveReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x15, packet: p)
|
||||
}
|
||||
case 0x16:
|
||||
if let p = packet as? PacketGroupBan {
|
||||
onGroupBanReceived?(p)
|
||||
notifyGroupOneShotHandlers(packetId: 0x16, packet: p)
|
||||
}
|
||||
case 0x0F:
|
||||
if let p = packet as? PacketRequestTransport {
|
||||
|
||||
Reference in New Issue
Block a user