| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- //
- // 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() }
- }
- }
|