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