// // LNProfileManager.swift // Lanu // // Created by OneeChan on 2025/11/6. // import Foundation protocol LNProfileManagerNotify { func onUserInfoChanged(userInfo: LNUserProfileVO) func onBindPhoneCaptchaCoolDownChanged(time: Int) func onUserVoiceBarInfoChanged() } extension LNProfileManagerNotify { func onUserInfoChanged(userInfo: LNUserProfileVO) { } func onBindPhoneCaptchaCoolDownChanged(time: Int) { } func onUserVoiceBarInfoChanged() { } } var myUserInfo: LNUserProfileVO { LNProfileManager.shared.myUserInfo } var myVoiceBarInfo: LNUserVoiceStateVO { LNProfileManager.shared.myVoiceState } class LNProfileUserInfo { var uid: String = "" var name: String = "" var avatar: String = "" } class LNProfileManager { static let shared = LNProfileManager() static let nameMaxInput = 25 static let bioMaxInput = 100 fileprivate var myUserInfo: LNUserProfileVO = LNUserProfileVO() fileprivate var myVoiceState: LNUserVoiceStateVO = LNUserVoiceStateVO() private var randomProfile: LNRandomProfileResponse? private static let profileCacheLimit = 300 private var profileCache: [String: LNProfileUserInfo] = [:] private var profileCacheOrder: [String] = [] private let profileCacheLock = NSLock() private let captchaCoolDown = 60 private var captchaRemain = 0 private var captchaTimer: Timer? var canSendCaptcha: Bool { captchaRemain == 0 } private init() { LNEventDeliver.addObserver(self) } } extension LNProfileManager { func reloadMyProfileDetail() { LNHttpManager.shared.getMyProfileDetail { [weak self] res, err in guard let self else { return } guard err == nil, let res else { return } if let relation = res.userFollowStat { LNRelationManager.shared.updateMyRelationInfo(relation) } if let voice = res.userVoice { myVoiceState = voice notifyUserVoiceBarInfoChanged() } } getUserProfileDetail(uid: myUid) { _ in } } func modifyMyProfile(config: LNProfileUpdateConfig, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.modifyMyProfile( config: config) { [weak self] err in guard let self else { return } if let err { showToast(err.errorDesc) } else { reloadMyProfileDetail() if config.interest != nil { LNGameMateManager.shared.getGameTypeList { _ in } } } queue.asyncIfNotGlobal { handler(err == nil) } } } func setMyVoiceBar(url: String, duration: Int, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.setMyVoiceBar(url: url, duration: duration) { [weak self] err in queue.asyncIfNotGlobal { handler(err == nil) } if let err { showToast(err.errorDesc) } else { guard let self else { return } reloadMyProfileDetail() } } } func cleanVoiceBar(queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.cleanVoiceBar { [weak self] err in queue.asyncIfNotGlobal { handler(err == nil) } if let err { showToast(err.errorDesc) } else { guard let self else { return } reloadMyProfileDetail() } } } } extension LNProfileManager { func getUserProfileDetail(uid: String, queue: DispatchQueue = .main, handler: @escaping (LNUserProfileVO?) -> Void) { LNHttpManager.shared.getUsersInfo(uids: [uid]) { [weak self] res, err in guard let self else { return } if let res, err == nil { res.list.forEach { if $0.userNo.isMyUid { if self.myUserInfo.update($0) { self.notifyUserInfoChanged(newInfo: $0) } } else { self.updateProfileCache( uid: $0.userNo, name: $0.nickname, avatar: $0.avatar ) self.notifyUserInfoChanged(newInfo: $0) } } } else { showToast(err?.errorDesc) } queue.asyncIfNotGlobal { handler(res?.list.first) } } } func getRandomProfile(queue: DispatchQueue = .main, handler: @escaping (LNProfileRandomInfoVO?, LNProfileRandomInfoVO?) -> Void) { if let randomProfile { handler(randomProfile.male, randomProfile.female) return } LNHttpManager.shared.getRandomProfile { [weak self] res, err in if let self, let res { randomProfile = res } queue.asyncIfNotGlobal { handler(res?.male, res?.female) } } } // func getUserOnlineState(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([String: Bool]) -> Void) { // LNHttpManager.shared.getUserOnlineState(uids: uids) { res, err in // queue.asyncIfNotGlobal { // handler(res?.list.reduce(into: [String: Bool](), { partialResult, state in // partialResult[state.userNo] = state.online // }) ?? [:]) // } // } // } } extension LNProfileManager { func getCachedProfileUserInfo(uid: String) -> LNProfileUserInfo? { profileCacheLock.lock() defer { profileCacheLock.unlock() } guard let info = profileCache[uid] else { return nil } touchProfileCacheKey(uid) return info } func getCachedProfileUserInfo(uid: String, fetchIfNeeded: Bool = true, queue: DispatchQueue = .main, handler: @escaping (LNProfileUserInfo?) -> Void) { if let info = getCachedProfileUserInfo(uid: uid) { queue.asyncIfNotGlobal { handler(info) } return } guard fetchIfNeeded else { queue.asyncIfNotGlobal { handler(nil) } return } getUserProfileDetail(uid: uid, queue: queue) { [weak self] profile in guard let self, let profile else { handler(nil) return } self.updateProfileCache( uid: profile.userNo, name: profile.nickname, avatar: profile.avatar ) handler(self.getCachedProfileUserInfo(uid: uid)) } } func updateProfileCache(uid: String, name: String, avatar: String) { guard !uid.isEmpty else { return } profileCacheLock.lock() defer { profileCacheLock.unlock() } let info = profileCache[uid] ?? LNProfileUserInfo() info.uid = uid info.name = name info.avatar = avatar profileCache[uid] = info touchProfileCacheKey(uid) trimProfileCacheIfNeeded() } private func touchProfileCacheKey(_ uid: String) { if let index = profileCacheOrder.firstIndex(of: uid) { profileCacheOrder.remove(at: index) } profileCacheOrder.append(uid) } private func trimProfileCacheIfNeeded() { while profileCacheOrder.count > Self.profileCacheLimit { let expiredUid = profileCacheOrder.removeFirst() profileCache.removeValue(forKey: expiredUid) } } } extension LNProfileManager { func getBindPhoneCaptcha(code: String, phone: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.getBindPhoneCaptcha(code: code, phone: phone) { [weak self] err in queue.asyncIfNotGlobal { handler(err == nil) } guard let self else { return } if let err { showToast(err.errorDesc) } else { captchaRemain = captchaCoolDown notifyCaptchaTime(time: captchaRemain) startCaptchaTimer() } } } func bindPhone(code: String, phone: String, queue: DispatchQueue = .main, captcha: String, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.bindPhone(code: code, phone: phone, captcha: captcha) { err in queue.asyncIfNotGlobal { handler(err == nil) } if let err { showToast(err.errorDesc) } } } private func startCaptchaTimer() { DispatchQueue.main.async { [weak self] in guard let self else { return } stopCaptchaTimer() let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in guard let self else { return } captchaRemain -= 1 notifyCaptchaTime(time: captchaRemain) if captchaRemain == 0 { stopCaptchaTimer() } } RunLoop.main.add(timer, forMode: .common) captchaTimer = timer } } private func stopCaptchaTimer() { captchaTimer?.invalidate() captchaTimer = nil } } extension LNProfileManager { func reportCurrentLanguage(code: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { LNHttpManager.shared.reportCurrentLanguage(code: code) { err in queue.asyncIfNotGlobal { handler(err == nil) } } } } extension LNProfileManager: LNAccountManagerNotify { func onUserLogin() { reloadMyProfileDetail() } func onUserLogout() { myUserInfo = LNUserProfileVO() } } extension LNProfileManager { private func notifyUserInfoChanged(newInfo: LNUserProfileVO) { LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onUserInfoChanged(userInfo: newInfo) } } private func notifyCaptchaTime(time: Int) { LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onBindPhoneCaptchaCoolDownChanged(time: time) } } private func notifyUserVoiceBarInfoChanged() { LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onUserVoiceBarInfoChanged() } } }