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:
2026-02-25 21:27:41 +05:00
parent 7fb57fffba
commit 99a35302fa
54 changed files with 5818 additions and 213 deletions

View 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)
}
}
}