Compare commits
No commits in common. "71fb0551fe08eea3c2518d175dffcbd25a551322" and "1449e003de7e0180b1b4a11f5ee578ea435a9036" have entirely different histories.
71fb0551fe
...
1449e003de
@ -6,7 +6,6 @@ import UserNotifications
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate {
|
||||||
private let pushTokenManager = PushTokenManager.shared
|
|
||||||
|
|
||||||
func application(_ application: UIApplication,
|
func application(_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
@ -53,12 +52,6 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
|
|
||||||
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
|
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
|
||||||
guard let fcmToken else {
|
print("🔥 FCM token:", fcmToken ?? "NO TOKEN")
|
||||||
if AppConfig.DEBUG { print("🔥 FCM token: NO TOKEN") }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if AppConfig.DEBUG { print("🔥 FCM token:", fcmToken) }
|
|
||||||
pushTokenManager.registerFCMToken(fcmToken)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,50 +171,6 @@ final class SessionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatePushToken(_ token: String, completion: @escaping (Result<String, Error>) -> Void) {
|
|
||||||
client.request(
|
|
||||||
path: "/v1/auth/sessions/update_push_token",
|
|
||||||
method: .post,
|
|
||||||
query: ["fcm_token": token],
|
|
||||||
requiresAuth: true
|
|
||||||
) { [decoder] result in
|
|
||||||
switch result {
|
|
||||||
case .success(let response):
|
|
||||||
do {
|
|
||||||
let apiResponse = try decoder.decode(APIResponse<MessagePayload>.self, from: response.data)
|
|
||||||
guard apiResponse.status == "fine" else {
|
|
||||||
let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить push-токен.", comment: "Sessions service update push unexpected status")
|
|
||||||
completion(.failure(SessionsServiceError.unexpectedStatus(message)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.success(apiResponse.data.message))
|
|
||||||
} catch {
|
|
||||||
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
|
|
||||||
if AppConfig.DEBUG {
|
|
||||||
print("[SessionsService] decode update-push failed: \(debugMessage)")
|
|
||||||
}
|
|
||||||
completion(.failure(SessionsServiceError.decoding(debugDescription: debugMessage)))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
if case let NetworkError.server(_, data) = error,
|
|
||||||
let data,
|
|
||||||
let message = Self.errorMessage(from: data) {
|
|
||||||
completion(.failure(SessionsServiceError.unexpectedStatus(message)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePushToken(_ token: String) async throws -> String {
|
|
||||||
try await withCheckedThrowingContinuation { continuation in
|
|
||||||
updatePushToken(token) { result in
|
|
||||||
continuation.resume(with: result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func decodeDate(from decoder: Decoder) throws -> Date {
|
private static func decodeDate(from decoder: Decoder) throws -> Date {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let string = try container.decode(String.self)
|
let string = try container.decode(String.self)
|
||||||
|
|||||||
@ -1198,9 +1198,6 @@
|
|||||||
"Не удалось найти результаты." : {
|
"Не удалось найти результаты." : {
|
||||||
"comment" : "Search unexpected status"
|
"comment" : "Search unexpected status"
|
||||||
},
|
},
|
||||||
"Не удалось обновить push-токен." : {
|
|
||||||
"comment" : "Sessions service update push unexpected status"
|
|
||||||
},
|
|
||||||
"Не удалось обновить пароль." : {
|
"Не удалось обновить пароль." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@ -1,155 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class PushTokenManager {
|
|
||||||
static let shared = PushTokenManager()
|
|
||||||
|
|
||||||
private let queue = DispatchQueue(label: "org.yobble.push-token", qos: .utility)
|
|
||||||
private let sessionsService: SessionsService
|
|
||||||
private var currentFCMToken: String?
|
|
||||||
private var lastSentTokens: [String: String]
|
|
||||||
private var loginsRequiringSync: Set<String> = []
|
|
||||||
private var isUpdating = false
|
|
||||||
private var pendingUpdate = false
|
|
||||||
private var retryWorkItem: DispatchWorkItem?
|
|
||||||
private var notificationTokens: [NSObjectProtocol] = []
|
|
||||||
|
|
||||||
private enum Keys {
|
|
||||||
static let storedToken = "push.current_fcm_token"
|
|
||||||
static let sentTokens = "push.last_sent_tokens"
|
|
||||||
static let currentUser = "currentUser"
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum Constants {
|
|
||||||
static let retryDelay: TimeInterval = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(sessionsService: SessionsService = SessionsService()) {
|
|
||||||
self.sessionsService = sessionsService
|
|
||||||
let defaults = UserDefaults.standard
|
|
||||||
self.currentFCMToken = defaults.string(forKey: Keys.storedToken)
|
|
||||||
self.lastSentTokens = defaults.dictionary(forKey: Keys.sentTokens) as? [String: String] ?? [:]
|
|
||||||
observeNotifications()
|
|
||||||
|
|
||||||
queue.async { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
if let login = self.currentLogin() {
|
|
||||||
self.loginsRequiringSync.insert(login)
|
|
||||||
}
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
notificationTokens.forEach { NotificationCenter.default.removeObserver($0) }
|
|
||||||
notificationTokens.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerFCMToken(_ token: String) {
|
|
||||||
queue.async { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
guard self.currentFCMToken != token else { return }
|
|
||||||
|
|
||||||
self.currentFCMToken = token
|
|
||||||
UserDefaults.standard.set(token, forKey: Keys.storedToken)
|
|
||||||
|
|
||||||
if let login = self.currentLogin() {
|
|
||||||
self.loginsRequiringSync.insert(login)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func observeNotifications() {
|
|
||||||
let center = NotificationCenter.default
|
|
||||||
|
|
||||||
let accessTokenObserver = center.addObserver(forName: .accessTokenDidChange, object: nil, queue: nil) { [weak self] _ in
|
|
||||||
guard let self else { return }
|
|
||||||
self.queue.async {
|
|
||||||
if let login = self.currentLogin() {
|
|
||||||
self.loginsRequiringSync.insert(login)
|
|
||||||
} else {
|
|
||||||
self.loginsRequiringSync.removeAll()
|
|
||||||
}
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notificationTokens.append(accessTokenObserver)
|
|
||||||
|
|
||||||
let didBecomeActiveObserver = center.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
|
|
||||||
guard let self else { return }
|
|
||||||
self.queue.async {
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notificationTokens.append(didBecomeActiveObserver)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tryUpdateTokenIfNeeded() {
|
|
||||||
guard pendingUpdate else { return }
|
|
||||||
guard !isUpdating else { return }
|
|
||||||
guard let login = currentLogin() else { return }
|
|
||||||
guard let token = currentFCMToken, !token.isEmpty else { return }
|
|
||||||
|
|
||||||
let needsForcedSync = loginsRequiringSync.contains(login)
|
|
||||||
if !needsForcedSync, let lastToken = lastSentTokens[login], lastToken == token {
|
|
||||||
pendingUpdate = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingUpdate = false
|
|
||||||
isUpdating = true
|
|
||||||
retryWorkItem?.cancel()
|
|
||||||
retryWorkItem = nil
|
|
||||||
|
|
||||||
sessionsService.updatePushToken(token) { [weak self] result in
|
|
||||||
guard let self else { return }
|
|
||||||
self.queue.async {
|
|
||||||
self.isUpdating = false
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
self.loginsRequiringSync.remove(login)
|
|
||||||
self.lastSentTokens[login] = token
|
|
||||||
UserDefaults.standard.set(self.lastSentTokens, forKey: Keys.sentTokens)
|
|
||||||
if AppConfig.DEBUG {
|
|
||||||
print("[PushTokenManager] Push token updated for @\(login)")
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
if AppConfig.DEBUG {
|
|
||||||
print("[PushTokenManager] Failed to update push token: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
self.loginsRequiringSync.insert(login)
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.scheduleRetry()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scheduleRetry() {
|
|
||||||
guard retryWorkItem == nil else { return }
|
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
self.retryWorkItem = nil
|
|
||||||
self.pendingUpdate = true
|
|
||||||
self.tryUpdateTokenIfNeeded()
|
|
||||||
}
|
|
||||||
retryWorkItem = workItem
|
|
||||||
queue.asyncAfter(deadline: .now() + Constants.retryDelay, execute: workItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func currentLogin() -> String? {
|
|
||||||
guard let login = UserDefaults.standard.string(forKey: Keys.currentUser), !login.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return login
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -340,7 +340,17 @@ final class SocketService {
|
|||||||
private func handleNewPrivateMessage(_ data: [Any]) {
|
private func handleNewPrivateMessage(_ data: [Any]) {
|
||||||
guard let payload = data.first else { return }
|
guard let payload = data.first else { return }
|
||||||
|
|
||||||
guard let messageData = normalizeMessagePayload(payload) else { return }
|
let messageData: Data
|
||||||
|
if let dictionary = payload as? [String: Any],
|
||||||
|
JSONSerialization.isValidJSONObject(dictionary),
|
||||||
|
let json = try? JSONSerialization.data(withJSONObject: dictionary, options: []) {
|
||||||
|
messageData = json
|
||||||
|
} else if let string = payload as? String,
|
||||||
|
let data = string.data(using: .utf8) {
|
||||||
|
messageData = data
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
@ -363,31 +373,6 @@ final class SocketService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func normalizeMessagePayload(_ payload: Any) -> Data? {
|
|
||||||
// Server can wrap the actual message in an { event, payload } envelope.
|
|
||||||
if let dictionary = payload as? [String: Any] {
|
|
||||||
let messageBody = dictionary["payload"] ?? dictionary
|
|
||||||
if let messageDict = messageBody as? [String: Any],
|
|
||||||
JSONSerialization.isValidJSONObject(messageDict) {
|
|
||||||
return try? JSONSerialization.data(withJSONObject: messageDict, options: [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let string = payload as? String,
|
|
||||||
let data = string.data(using: .utf8) {
|
|
||||||
if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
|
||||||
let nested = jsonObject["payload"] {
|
|
||||||
return normalizeMessagePayload(nested)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
if let data = payload as? Data {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleHeartbeatSuccess() {
|
private func handleHeartbeatSuccess() {
|
||||||
consecutiveHeartbeatMisses = 0
|
consecutiveHeartbeatMisses = 0
|
||||||
heartbeatAckInFlight = false
|
heartbeatAckInFlight = false
|
||||||
@ -468,3 +453,7 @@ final class SocketService {
|
|||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let socketDidReceivePrivateMessage = Notification.Name("socketDidReceivePrivateMessage")
|
static let socketDidReceivePrivateMessage = Notification.Name("socketDidReceivePrivateMessage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//[SocketService] Failed to decode new message: typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "messageId", intValue: nil)], debugDescription: "Expected to decode String or number for key messageId", underlyingError: nil))
|
||||||
|
//[SocketService] payload={"event":"chat_private:new_message","payload":{"is_viewed":false,"content":"Ttyijfff","message_id":241,"message_type":["text"],"chat_id":"838146ed-1251-42df-b529-da7870101fa3","media_link":null,"created_at":"2025-11-27T23:10:09.039724+00:00","updated_at":null,"sender_data":{"full_name":"Системный Админ 1","is_verified":true,"is_system":false,"rating":{"status":"fine","rating":5},"custom_name":null,"login":"admin","created_at":"2025-10-20T18:19:04.911483Z","stories":[],"user_id":"7a319996-8e6a-4cc4-a808-091eda7cea6f","permissions":{"you_can_call_permission":true,"you_can_public_invite_permission":true,"you_can_send_message":true,"you_can_group_invite_permission":true},"bio":null,"profile_permissions":{"allow_messages_from_non_contacts":true,"max_message_auto_delete_seconds":null,"force_auto_delete_messages_in_private":false,"is_searchable":true,"allow_message_forwarding":true,"allow_server_chats":true},"last_seen":3300664,"relationship":{"is_current_user_in_contacts_of_target":false,"is_current_user_in_blacklist_of_target":false,"is_target_user_blocked_by_current_user":false}},"forward_metadata":null,"sender_id":"7a319996-8e6a-4cc4-a808-091eda7cea6f"}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user