feat: Implement chat list and search functionality
- Added ChatListViewModel to manage chat list state and server search. - Created ChatRowView for displaying individual chat rows. - Developed SearchView and SearchViewModel for user search functionality. - Introduced MainTabView for tab-based navigation between chats and settings. - Implemented OnboardingPager for onboarding experience. - Created SettingsView and SettingsViewModel for user settings management. - Added SplashView for initial app launch experience.
This commit is contained in:
302
Rosetta/Core/Network/Protocol/ProtocolManager.swift
Normal file
302
Rosetta/Core/Network/Protocol/ProtocolManager.swift
Normal file
@@ -0,0 +1,302 @@
|
||||
import Foundation
|
||||
import os
|
||||
import Observation
|
||||
import UIKit
|
||||
|
||||
// MARK: - Connection State
|
||||
|
||||
enum ConnectionState: String {
|
||||
case disconnected
|
||||
case connecting
|
||||
case connected
|
||||
case handshaking
|
||||
case authenticated
|
||||
}
|
||||
|
||||
// MARK: - ProtocolManager
|
||||
|
||||
/// Central networking coordinator. Owns WebSocket, routes packets, manages handshake.
|
||||
@Observable
|
||||
final class ProtocolManager: @unchecked Sendable {
|
||||
|
||||
static let shared = ProtocolManager()
|
||||
|
||||
private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "Protocol")
|
||||
|
||||
// MARK: - Public State
|
||||
|
||||
private(set) var connectionState: ConnectionState = .disconnected
|
||||
|
||||
// MARK: - Callbacks
|
||||
|
||||
var onMessageReceived: ((PacketMessage) -> Void)?
|
||||
var onDeliveryReceived: ((PacketDelivery) -> Void)?
|
||||
var onReadReceived: ((PacketRead) -> Void)?
|
||||
var onOnlineStateReceived: ((PacketOnlineState) -> Void)?
|
||||
var onUserInfoReceived: ((PacketUserInfo) -> Void)?
|
||||
var onSearchResult: ((PacketSearch) -> Void)?
|
||||
var onTypingReceived: ((PacketTyping) -> Void)?
|
||||
var onSyncReceived: ((PacketSync) -> Void)?
|
||||
var onHandshakeCompleted: ((PacketHandshake) -> Void)?
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private let client = WebSocketClient()
|
||||
private var packetQueue: [any Packet] = []
|
||||
private var handshakeComplete = false
|
||||
private var heartbeatTask: Task<Void, Never>?
|
||||
private var handshakeTimeoutTask: Task<Void, Never>?
|
||||
|
||||
// Saved credentials for auto-reconnect
|
||||
private var savedPublicKey: String?
|
||||
private var savedPrivateHash: String?
|
||||
|
||||
var publicKey: String? { savedPublicKey }
|
||||
var privateHash: String? { savedPrivateHash }
|
||||
|
||||
private init() {
|
||||
setupClientCallbacks()
|
||||
}
|
||||
|
||||
// MARK: - Connection
|
||||
|
||||
/// Connect to server and perform handshake.
|
||||
func connect(publicKey: String, privateKeyHash: String) {
|
||||
savedPublicKey = publicKey
|
||||
savedPrivateHash = privateKeyHash
|
||||
|
||||
if connectionState == .authenticated || connectionState == .handshaking {
|
||||
Self.logger.info("Already connected/handshaking, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
connectionState = .connecting
|
||||
client.connect()
|
||||
}
|
||||
|
||||
func disconnect() {
|
||||
Self.logger.info("Disconnecting")
|
||||
heartbeatTask?.cancel()
|
||||
handshakeTimeoutTask?.cancel()
|
||||
handshakeComplete = false
|
||||
client.disconnect()
|
||||
connectionState = .disconnected
|
||||
savedPublicKey = nil
|
||||
savedPrivateHash = nil
|
||||
}
|
||||
|
||||
// MARK: - Sending
|
||||
|
||||
func sendPacket(_ packet: any Packet) {
|
||||
if !handshakeComplete && !(packet is PacketHandshake) {
|
||||
Self.logger.info("Queueing packet \(type(of: packet).packetId)")
|
||||
packetQueue.append(packet)
|
||||
return
|
||||
}
|
||||
sendPacketDirect(packet)
|
||||
}
|
||||
|
||||
// MARK: - Private Setup
|
||||
|
||||
private func setupClientCallbacks() {
|
||||
client.onConnected = { [weak self] in
|
||||
guard let self else { return }
|
||||
Self.logger.info("WebSocket connected")
|
||||
|
||||
Task { @MainActor in
|
||||
self.connectionState = .connected
|
||||
}
|
||||
|
||||
// Auto-handshake with saved credentials
|
||||
if let pk = savedPublicKey, let hash = savedPrivateHash {
|
||||
startHandshake(publicKey: pk, privateHash: hash)
|
||||
}
|
||||
}
|
||||
|
||||
client.onDisconnected = { [weak self] error in
|
||||
guard let self else { return }
|
||||
if let error {
|
||||
Self.logger.error("Disconnected: \(error.localizedDescription)")
|
||||
}
|
||||
heartbeatTask?.cancel()
|
||||
handshakeComplete = false
|
||||
|
||||
Task { @MainActor in
|
||||
self.connectionState = .disconnected
|
||||
}
|
||||
}
|
||||
|
||||
client.onDataReceived = { [weak self] data in
|
||||
self?.handleIncomingData(data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Handshake
|
||||
|
||||
private func startHandshake(publicKey: String, privateHash: String) {
|
||||
Self.logger.info("Starting handshake for \(publicKey.prefix(20))...")
|
||||
|
||||
Task { @MainActor in
|
||||
connectionState = .handshaking
|
||||
}
|
||||
|
||||
let device = HandshakeDevice(
|
||||
deviceId: UIDevice.current.identifierForVendor?.uuidString ?? "unknown",
|
||||
deviceName: UIDevice.current.name,
|
||||
deviceOs: "iOS \(UIDevice.current.systemVersion)"
|
||||
)
|
||||
|
||||
let handshake = PacketHandshake(
|
||||
privateKey: privateHash,
|
||||
publicKey: publicKey,
|
||||
protocolVersion: 1,
|
||||
heartbeatInterval: 15,
|
||||
device: device,
|
||||
handshakeState: .needDeviceVerification
|
||||
)
|
||||
|
||||
sendPacketDirect(handshake)
|
||||
|
||||
// Timeout
|
||||
handshakeTimeoutTask?.cancel()
|
||||
handshakeTimeoutTask = Task { [weak self] in
|
||||
do {
|
||||
try await Task.sleep(nanoseconds: 10_000_000_000)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
guard let self, !Task.isCancelled else { return }
|
||||
if !self.handshakeComplete {
|
||||
Self.logger.error("Handshake timeout")
|
||||
self.client.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Packet Handling
|
||||
|
||||
private func handleIncomingData(_ data: Data) {
|
||||
print("[Protocol] Incoming data: \(data.count) bytes, first bytes: \(data.prefix(min(8, data.count)).map { String(format: "%02x", $0) }.joined(separator: " "))")
|
||||
|
||||
guard let (packetId, packet) = PacketRegistry.decode(from: data) else {
|
||||
// Try to read the packet ID manually to see what it is
|
||||
if data.count >= 2 {
|
||||
let stream = Stream(data: data)
|
||||
let rawId = stream.readInt16()
|
||||
print("[Protocol] Unknown packet ID: 0x\(String(rawId, radix: 16)) (\(rawId)), data size: \(data.count)")
|
||||
} else {
|
||||
print("[Protocol] Packet too small: \(data.count) bytes")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
print("[Protocol] Received packet 0x\(String(packetId, radix: 16)) (\(type(of: packet)))")
|
||||
|
||||
switch packetId {
|
||||
case 0x00:
|
||||
if let p = packet as? PacketHandshake {
|
||||
handleHandshakeResponse(p)
|
||||
}
|
||||
case 0x01:
|
||||
if let p = packet as? PacketUserInfo {
|
||||
print("[Protocol] UserInfo received: username='\(p.username)', title='\(p.title)'")
|
||||
onUserInfoReceived?(p)
|
||||
}
|
||||
case 0x02:
|
||||
if let p = packet as? PacketResult {
|
||||
let code = ResultCode(rawValue: p.resultCode)
|
||||
print("[Protocol] Result received: code=\(p.resultCode) (\(code.map { "\($0)" } ?? "unknown"))")
|
||||
}
|
||||
case 0x03:
|
||||
if let p = packet as? PacketSearch {
|
||||
print("[Protocol] Search result received: \(p.users.count) users")
|
||||
onSearchResult?(p)
|
||||
}
|
||||
case 0x05:
|
||||
if let p = packet as? PacketOnlineState {
|
||||
onOnlineStateReceived?(p)
|
||||
}
|
||||
case 0x06:
|
||||
if let p = packet as? PacketMessage {
|
||||
onMessageReceived?(p)
|
||||
}
|
||||
case 0x07:
|
||||
if let p = packet as? PacketRead {
|
||||
onReadReceived?(p)
|
||||
}
|
||||
case 0x08:
|
||||
if let p = packet as? PacketDelivery {
|
||||
onDeliveryReceived?(p)
|
||||
}
|
||||
case 0x0B:
|
||||
if let p = packet as? PacketTyping {
|
||||
onTypingReceived?(p)
|
||||
}
|
||||
case 0x19:
|
||||
if let p = packet as? PacketSync {
|
||||
onSyncReceived?(p)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func handleHandshakeResponse(_ packet: PacketHandshake) {
|
||||
// Set handshakeComplete BEFORE cancelling timeout to prevent race
|
||||
handshakeComplete = true
|
||||
handshakeTimeoutTask?.cancel()
|
||||
handshakeTimeoutTask = nil
|
||||
|
||||
switch packet.handshakeState {
|
||||
case .completed:
|
||||
Self.logger.info("Handshake completed. Protocol v\(packet.protocolVersion), heartbeat \(packet.heartbeatInterval)s")
|
||||
|
||||
Task { @MainActor in
|
||||
self.connectionState = .authenticated
|
||||
}
|
||||
|
||||
flushPacketQueue()
|
||||
startHeartbeat(interval: packet.heartbeatInterval)
|
||||
onHandshakeCompleted?(packet)
|
||||
|
||||
case .needDeviceVerification:
|
||||
Self.logger.info("Server requires device verification")
|
||||
startHeartbeat(interval: packet.heartbeatInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Heartbeat
|
||||
|
||||
private func startHeartbeat(interval: Int) {
|
||||
heartbeatTask?.cancel()
|
||||
let intervalMs = UInt64(interval) * 1_000_000_000 / 3
|
||||
|
||||
heartbeatTask = Task {
|
||||
// Send first heartbeat immediately
|
||||
client.sendText("heartbeat")
|
||||
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: intervalMs)
|
||||
guard !Task.isCancelled else { break }
|
||||
client.sendText("heartbeat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Packet Queue
|
||||
|
||||
private func sendPacketDirect(_ packet: any Packet) {
|
||||
let data = PacketRegistry.encode(packet)
|
||||
Self.logger.info("Sending packet 0x\(String(type(of: packet).packetId, radix: 16)) (\(data.count) bytes)")
|
||||
client.send(data)
|
||||
}
|
||||
|
||||
private func flushPacketQueue() {
|
||||
Self.logger.info("Flushing \(self.packetQueue.count) queued packets")
|
||||
let packets = packetQueue
|
||||
packetQueue.removeAll()
|
||||
for packet in packets {
|
||||
sendPacketDirect(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user