socket patch
This commit is contained in:
		
							parent
							
								
									2b96e49d6f
								
							
						
					
					
						commit
						d8793b0947
					
				@ -27,8 +27,14 @@ struct TopBarView: View {
 | 
			
		||||
        return title == "Profile"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var shouldShowConnectionStatus: Bool {
 | 
			
		||||
        viewModel.socketState != .connected
 | 
			
		||||
    private var statusMessage: String? {
 | 
			
		||||
        if viewModel.socketState != .connected {
 | 
			
		||||
            return NSLocalizedString("Подключение", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
        if viewModel.chatLoadingState == .loading {
 | 
			
		||||
            return NSLocalizedString("Загрузка чатов", comment: "")
 | 
			
		||||
        }
 | 
			
		||||
        return nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var body: some View {
 | 
			
		||||
@ -47,8 +53,8 @@ struct TopBarView: View {
 | 
			
		||||
                
 | 
			
		||||
//                Spacer()
 | 
			
		||||
                
 | 
			
		||||
                if shouldShowConnectionStatus {
 | 
			
		||||
                    connectionStatusView
 | 
			
		||||
                if let statusMessage {
 | 
			
		||||
                    connectionStatusView(message: statusMessage)
 | 
			
		||||
                    Spacer()
 | 
			
		||||
                } else if isHomeTab{
 | 
			
		||||
                    Text("Yobble")
 | 
			
		||||
@ -136,11 +142,11 @@ struct TopBarView: View {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private extension TopBarView {
 | 
			
		||||
    var connectionStatusView: some View {
 | 
			
		||||
    func connectionStatusView(message: String) -> some View {
 | 
			
		||||
        HStack(spacing: 8) {
 | 
			
		||||
            ProgressView()
 | 
			
		||||
                .progressViewStyle(.circular)
 | 
			
		||||
            Text(NSLocalizedString("Подключение", comment: ""))
 | 
			
		||||
            Text(message)
 | 
			
		||||
                .font(.headline)
 | 
			
		||||
                .foregroundColor(.primary)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -430,6 +430,16 @@
 | 
			
		||||
    },
 | 
			
		||||
    "Загрузка сообщений…" : {
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
    "Загрузка чатов" : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
        "en" : {
 | 
			
		||||
          "stringUnit" : {
 | 
			
		||||
            "state" : "translated",
 | 
			
		||||
            "value" : "Loading chats"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "Загрузка..." : {
 | 
			
		||||
      "localizations" : {
 | 
			
		||||
@ -2277,4 +2287,4 @@
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "version" : "1.0"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -30,11 +30,16 @@ final class SocketService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private let heartbeatInterval: TimeInterval = 10
 | 
			
		||||
    private let heartbeatTimeout: TimeInterval = 5
 | 
			
		||||
    private let maxHeartbeatMissCount = 2
 | 
			
		||||
    private let heartbeatEventName = AppConfig.SOCKET_HEARTBEAT_EVENT
 | 
			
		||||
 | 
			
		||||
    #if canImport(SocketIO)
 | 
			
		||||
    private var manager: SocketManager?
 | 
			
		||||
    private var socket: SocketIOClient?
 | 
			
		||||
    private var heartbeatTimer: DispatchSourceTimer?
 | 
			
		||||
    private var heartbeatAckInFlight = false
 | 
			
		||||
    private var consecutiveHeartbeatMisses = 0
 | 
			
		||||
    #endif
 | 
			
		||||
 | 
			
		||||
    private init() {}
 | 
			
		||||
@ -150,13 +155,13 @@ final class SocketService {
 | 
			
		||||
 | 
			
		||||
        socket.on(clientEvent: .connect) { _, _ in
 | 
			
		||||
            if AppConfig.DEBUG { print("[SocketService] Connected") }
 | 
			
		||||
            self.updateConnectionState(.connected)
 | 
			
		||||
            self.handleHeartbeatSuccess()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        socket.on(clientEvent: .statusChange) { data, _ in
 | 
			
		||||
            guard let rawStatus = data.first as? SocketIOStatus else { return }
 | 
			
		||||
            if rawStatus == .connected {
 | 
			
		||||
                self.updateConnectionState(.connected)
 | 
			
		||||
                self.handleHeartbeatSuccess()
 | 
			
		||||
            } else if rawStatus == .connecting {
 | 
			
		||||
                self.updateConnectionState(.connecting)
 | 
			
		||||
            } else {
 | 
			
		||||
@ -184,6 +189,10 @@ final class SocketService {
 | 
			
		||||
            self.updateConnectionState(.disconnected)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        socket.on("pong") { [weak self] _, _ in
 | 
			
		||||
            self?.handleHeartbeatSuccess()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.manager = manager
 | 
			
		||||
        self.socket = socket
 | 
			
		||||
    }
 | 
			
		||||
@ -204,23 +213,7 @@ final class SocketService {
 | 
			
		||||
        timer.schedule(deadline: .now() + heartbeatInterval, repeating: heartbeatInterval)
 | 
			
		||||
        timer.setEventHandler { [weak self] in
 | 
			
		||||
            guard let self else { return }
 | 
			
		||||
 | 
			
		||||
            guard let socket = self.socket else {
 | 
			
		||||
                self.updateConnectionState(.disconnected)
 | 
			
		||||
                return
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            switch socket.status {
 | 
			
		||||
            case .connected:
 | 
			
		||||
                self.updateConnectionState(.connected)
 | 
			
		||||
            case .connecting:
 | 
			
		||||
                self.updateConnectionState(.connecting)
 | 
			
		||||
            case .disconnected, .notConnected:
 | 
			
		||||
                self.updateConnectionState(.connecting)
 | 
			
		||||
                socket.connect(withPayload: self.currentAuthPayload)
 | 
			
		||||
            @unknown default:
 | 
			
		||||
                self.updateConnectionState(.connecting)
 | 
			
		||||
            }
 | 
			
		||||
            self.performHeartbeatCheck()
 | 
			
		||||
        }
 | 
			
		||||
        heartbeatTimer = timer
 | 
			
		||||
        timer.resume()
 | 
			
		||||
@ -229,6 +222,76 @@ final class SocketService {
 | 
			
		||||
    private func stopHeartbeat() {
 | 
			
		||||
        heartbeatTimer?.cancel()
 | 
			
		||||
        heartbeatTimer = nil
 | 
			
		||||
        heartbeatAckInFlight = false
 | 
			
		||||
        consecutiveHeartbeatMisses = 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func performHeartbeatCheck() {
 | 
			
		||||
        guard let socket = socket else {
 | 
			
		||||
            updateConnectionState(.disconnected)
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch socket.status {
 | 
			
		||||
        case .connected:
 | 
			
		||||
            sendHeartbeat(on: socket)
 | 
			
		||||
        case .connecting:
 | 
			
		||||
            updateConnectionState(.connecting)
 | 
			
		||||
        case .disconnected, .notConnected:
 | 
			
		||||
            updateConnectionState(.connecting)
 | 
			
		||||
            socket.connect(withPayload: currentAuthPayload)
 | 
			
		||||
        @unknown default:
 | 
			
		||||
            updateConnectionState(.connecting)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func sendHeartbeat(on socket: SocketIOClient) {
 | 
			
		||||
        guard !heartbeatAckInFlight else { return }
 | 
			
		||||
        heartbeatAckInFlight = true
 | 
			
		||||
 | 
			
		||||
        socket.emitWithAck(heartbeatEventName, ["timestamp": Date().timeIntervalSince1970])
 | 
			
		||||
            .timingOut(after: heartbeatTimeout) { [weak self] data in
 | 
			
		||||
                guard let self else { return }
 | 
			
		||||
                self.heartbeatAckInFlight = false
 | 
			
		||||
 | 
			
		||||
                if self.isSuccessfulHeartbeatResponse(data) {
 | 
			
		||||
                    self.handleHeartbeatSuccess()
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.handleMissedHeartbeat(for: socket)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func isSuccessfulHeartbeatResponse(_ data: [Any]) -> Bool {
 | 
			
		||||
        guard !data.isEmpty else { return true }
 | 
			
		||||
 | 
			
		||||
        if let stringValue = data.first as? String, stringValue.uppercased() == "NO ACK" {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let ackStatus = data.first as? SocketAckStatus, ackStatus == .noAck {
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func handleHeartbeatSuccess() {
 | 
			
		||||
        consecutiveHeartbeatMisses = 0
 | 
			
		||||
        updateConnectionState(.connected)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func handleMissedHeartbeat(for socket: SocketIOClient) {
 | 
			
		||||
        consecutiveHeartbeatMisses += 1
 | 
			
		||||
        if consecutiveHeartbeatMisses <= maxHeartbeatMissCount {
 | 
			
		||||
            updateConnectionState(.connecting)
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        updateConnectionState(.connecting)
 | 
			
		||||
        consecutiveHeartbeatMisses = 0
 | 
			
		||||
        socket.disconnect()
 | 
			
		||||
        socket.connect(withPayload: currentAuthPayload)
 | 
			
		||||
    }
 | 
			
		||||
    #else
 | 
			
		||||
    private func disconnectInternal() { }
 | 
			
		||||
 | 
			
		||||
@ -17,11 +17,17 @@ class LoginViewModel: ObservableObject {
 | 
			
		||||
    @Published var errorMessage: String = ""
 | 
			
		||||
    @Published var isLoggedIn: Bool = false
 | 
			
		||||
    @Published var socketState: SocketService.ConnectionState
 | 
			
		||||
    @Published var chatLoadingState: ChatLoadingState = .idle
 | 
			
		||||
 | 
			
		||||
    private let authService = AuthService()
 | 
			
		||||
    private let socketService = SocketService.shared
 | 
			
		||||
    private var cancellables = Set<AnyCancellable>()
 | 
			
		||||
 | 
			
		||||
    enum ChatLoadingState: Equatable {
 | 
			
		||||
        case idle
 | 
			
		||||
        case loading
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private enum DefaultsKeys {
 | 
			
		||||
        static let currentUser = "currentUser"
 | 
			
		||||
        static let userId = "userId"
 | 
			
		||||
@ -30,6 +36,7 @@ class LoginViewModel: ObservableObject {
 | 
			
		||||
    init() {
 | 
			
		||||
        socketState = socketService.currentConnectionState
 | 
			
		||||
        observeSocketState()
 | 
			
		||||
        observeChatsReload()
 | 
			
		||||
//        loadStoredUser()
 | 
			
		||||
 | 
			
		||||
        // Запускаем автологин
 | 
			
		||||
@ -40,11 +47,35 @@ class LoginViewModel: ObservableObject {
 | 
			
		||||
        socketService.connectionStatePublisher
 | 
			
		||||
            .receive(on: DispatchQueue.main)
 | 
			
		||||
            .sink { [weak self] state in
 | 
			
		||||
                self?.socketState = state
 | 
			
		||||
                self?.handleSocketStateChange(state)
 | 
			
		||||
            }
 | 
			
		||||
            .store(in: &cancellables)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func observeChatsReload() {
 | 
			
		||||
        NotificationCenter.default.publisher(for: .chatsReloadCompleted)
 | 
			
		||||
            .receive(on: DispatchQueue.main)
 | 
			
		||||
            .sink { [weak self] _ in
 | 
			
		||||
                self?.chatLoadingState = .idle
 | 
			
		||||
            }
 | 
			
		||||
            .store(in: &cancellables)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func handleSocketStateChange(_ state: SocketService.ConnectionState) {
 | 
			
		||||
        socketState = state
 | 
			
		||||
        if state == .connected {
 | 
			
		||||
            triggerChatsReloadIfNeeded()
 | 
			
		||||
        } else {
 | 
			
		||||
            chatLoadingState = .idle
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private func triggerChatsReloadIfNeeded() {
 | 
			
		||||
        guard chatLoadingState != .loading else { return }
 | 
			
		||||
        chatLoadingState = .loading
 | 
			
		||||
        NotificationCenter.default.post(name: .chatsShouldRefresh, object: nil)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    func autoLogin() {
 | 
			
		||||
        authService.autoLogin { [weak self] success, error in
 | 
			
		||||
            DispatchQueue.main.async {
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,9 @@ final class PrivateChatsViewModel: ObservableObject {
 | 
			
		||||
        chatService.fetchPrivateChats(offset: 0, limit: pageSize) { [weak self] result in
 | 
			
		||||
            guard let self else { return }
 | 
			
		||||
            self.isInitialLoading = false
 | 
			
		||||
            defer {
 | 
			
		||||
                NotificationCenter.default.post(name: .chatsReloadCompleted, object: nil)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            switch result {
 | 
			
		||||
            case .success(let data):
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,9 @@ struct ChatsTab: View {
 | 
			
		||||
            .onReceive(NotificationCenter.default.publisher(for: .debugRefreshChats)) { _ in
 | 
			
		||||
                viewModel.refresh()
 | 
			
		||||
            }
 | 
			
		||||
            .onReceive(NotificationCenter.default.publisher(for: .chatsShouldRefresh)) { _ in
 | 
			
		||||
                viewModel.refresh()
 | 
			
		||||
            }
 | 
			
		||||
            .onChange(of: searchText) { newValue in
 | 
			
		||||
                handleSearchQueryChange(newValue)
 | 
			
		||||
                let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines)
 | 
			
		||||
@ -1146,4 +1149,6 @@ struct ChatsTab_Previews: PreviewProvider {
 | 
			
		||||
 | 
			
		||||
extension Notification.Name {
 | 
			
		||||
    static let debugRefreshChats = Notification.Name("debugRefreshChats")
 | 
			
		||||
    static let chatsShouldRefresh = Notification.Name("chatsShouldRefresh")
 | 
			
		||||
    static let chatsReloadCompleted = Notification.Name("chatsReloadCompleted")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ struct AppConfig {
 | 
			
		||||
    static let PROTOCOL = "https"
 | 
			
		||||
    static let API_SERVER = "\(PROTOCOL)://api.yobble.org"
 | 
			
		||||
    static let SOCKET_PATH = "/socket.io/"
 | 
			
		||||
    static let SOCKET_HEARTBEAT_EVENT = "ping"
 | 
			
		||||
 | 
			
		||||
    static let USER_AGENT = "yobble ios"
 | 
			
		||||
    static let APP_BUILD = "appstore" // appstore / freestore
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user