Уведомления, Real-time синхронизация, фотки, reply and forward

This commit is contained in:
2026-03-17 03:51:29 +05:00
parent 624038915d
commit 1f442e1298
26 changed files with 2711 additions and 431 deletions

View File

@@ -105,50 +105,17 @@ final class ProtocolManager: @unchecked Sendable {
}
/// Verify connection health after returning from background.
/// If connection appears alive, sends a WebSocket ping to confirm.
/// If ping fails or times out (2s), forces immediate reconnection.
/// Always force reconnect after background, the socket is likely dead
/// and a 2s ping timeout just delays the inevitable.
func reconnectIfNeeded() {
guard savedPublicKey != nil, savedPrivateHash != nil else { return }
// Don't interrupt active handshake
if connectionState == .handshaking { return }
if connectionState == .authenticated && client.isConnected {
// Connection looks alive verify with ping (2s timeout)
Self.logger.info("Verifying connection with ping...")
let pingTimeoutTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 2_000_000_000)
guard !Task.isCancelled, let self else { return }
Self.logger.info("Ping timeout — connection dead, force reconnecting")
self.handshakeComplete = false
self.heartbeatTask?.cancel()
Task { @MainActor in
self.connectionState = .connecting
}
self.client.forceReconnect()
}
client.sendPing { [weak self] error in
pingTimeoutTask.cancel()
guard let self else { return }
if let error {
Self.logger.info("Ping failed: \(error.localizedDescription) — force reconnecting")
self.handshakeComplete = false
self.heartbeatTask?.cancel()
Task { @MainActor in
self.connectionState = .connecting
}
self.client.forceReconnect()
} else {
Self.logger.info("Ping succeeded — connection alive")
}
}
return
}
// Not authenticated or not connected force reconnect immediately
Self.logger.info("Force reconnect from foreground")
Self.logger.info("Foreground reconnect — force reconnecting")
handshakeComplete = false
heartbeatTask?.cancel()
connectionState = .connecting
client.forceReconnect()
}
@@ -234,6 +201,18 @@ final class ProtocolManager: @unchecked Sendable {
client.onDataReceived = { [weak self] data in
self?.handleIncomingData(data)
}
// Instant reconnect when network is restored (Wi-Fi cellular, airplane mode off, etc.)
client.onNetworkRestored = { [weak self] in
guard let self, self.savedPublicKey != nil else { return }
Self.logger.info("Network restored — force reconnecting")
self.handshakeComplete = false
self.heartbeatTask?.cancel()
Task { @MainActor in
self.connectionState = .connecting
}
self.client.forceReconnect()
}
}
// MARK: - Handshake
@@ -399,17 +378,21 @@ final class ProtocolManager: @unchecked Sendable {
handshakeComplete = true
Self.logger.info("Handshake completed. Protocol v\(packet.protocolVersion), heartbeat \(packet.heartbeatInterval)s")
Task { @MainActor in
self.connectionState = .authenticated
}
flushPacketQueue()
startHeartbeat(interval: packet.heartbeatInterval)
// Desktop parity: request transport server URL after handshake.
sendPacketDirect(PacketRequestTransport())
onHandshakeCompleted?(packet)
// CRITICAL: set .authenticated and fire callback in ONE MainActor task.
// Previously these were separate tasks Swift doesn't guarantee FIFO
// ordering of unstructured tasks, so requestSynchronize() could race
// with the state change and silently drop the sync request.
let callback = self.onHandshakeCompleted
Task { @MainActor in
self.connectionState = .authenticated
callback?(packet)
}
case .needDeviceVerification:
handshakeComplete = false
@@ -429,8 +412,8 @@ final class ProtocolManager: @unchecked Sendable {
private func startHeartbeat(interval: Int) {
heartbeatTask?.cancel()
// Desktop parity: heartbeat at half the server-specified interval.
let intervalNs = UInt64(interval) * 1_000_000_000 / 2
// Android parity: heartbeat at 1/3 the server-specified interval (more aggressive keep-alive).
let intervalNs = UInt64(interval) * 1_000_000_000 / 3
heartbeatTask = Task {
// Send first heartbeat immediately