LNProfileManager.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. //
  2. // LNProfileManager.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/6.
  6. //
  7. import Foundation
  8. protocol LNProfileManagerNotify {
  9. func onUserInfoChanged(userInfo: LNUserProfileVO)
  10. func onBindPhoneCaptchaCoolDownChanged(time: Int)
  11. func onUserVoiceBarInfoChanged()
  12. }
  13. extension LNProfileManagerNotify {
  14. func onUserInfoChanged(userInfo: LNUserProfileVO) { }
  15. func onBindPhoneCaptchaCoolDownChanged(time: Int) { }
  16. func onUserVoiceBarInfoChanged() { }
  17. }
  18. var myUserInfo: LNUserProfileVO {
  19. LNProfileManager.shared.myUserInfo
  20. }
  21. var myVoiceBarInfo: LNUserVoiceStateVO {
  22. LNProfileManager.shared.myVoiceState
  23. }
  24. class LNProfileUserInfo {
  25. var uid: String = ""
  26. var name: String = ""
  27. var avatar: String = ""
  28. }
  29. class LNProfileManager {
  30. static let shared = LNProfileManager()
  31. static let nameMaxInput = 25
  32. static let bioMaxInput = 100
  33. fileprivate var myUserInfo: LNUserProfileVO = LNUserProfileVO()
  34. fileprivate var myVoiceState: LNUserVoiceStateVO = LNUserVoiceStateVO()
  35. private var randomProfile: LNRandomProfileResponse?
  36. private static let profileCacheLimit = 300
  37. private var profileCache: [String: LNProfileUserInfo] = [:]
  38. private var profileCacheOrder: [String] = []
  39. private let profileCacheLock = NSLock()
  40. private let captchaCoolDown = 60
  41. private var captchaRemain = 0
  42. private var captchaTimer: Timer?
  43. var canSendCaptcha: Bool {
  44. captchaRemain == 0
  45. }
  46. private init() {
  47. LNEventDeliver.addObserver(self)
  48. }
  49. }
  50. extension LNProfileManager {
  51. func reloadMyProfileDetail() {
  52. LNHttpManager.shared.getMyProfileDetail { [weak self] res, err in
  53. guard let self else { return }
  54. guard err == nil, let res else { return }
  55. if let relation = res.userFollowStat {
  56. LNRelationManager.shared.updateMyRelationInfo(relation)
  57. }
  58. if let voice = res.userVoice {
  59. myVoiceState = voice
  60. notifyUserVoiceBarInfoChanged()
  61. }
  62. }
  63. getUserProfileDetail(uid: myUid) { _ in }
  64. }
  65. func modifyMyProfile(config: LNProfileUpdateConfig, queue: DispatchQueue = .main,
  66. handler: @escaping (Bool) -> Void) {
  67. LNHttpManager.shared.modifyMyProfile(
  68. config: config) { [weak self] err in
  69. guard let self else { return }
  70. if let err {
  71. showToast(err.errorDesc)
  72. } else {
  73. reloadMyProfileDetail()
  74. if config.interest != nil {
  75. LNGameMateManager.shared.getGameTypeList { _ in }
  76. }
  77. }
  78. queue.asyncIfNotGlobal {
  79. handler(err == nil)
  80. }
  81. }
  82. }
  83. func setMyVoiceBar(url: String, duration: Int, queue: DispatchQueue = .main,
  84. handler: @escaping (Bool) -> Void) {
  85. LNHttpManager.shared.setMyVoiceBar(url: url, duration: duration) { [weak self] err in
  86. queue.asyncIfNotGlobal {
  87. handler(err == nil)
  88. }
  89. if let err {
  90. showToast(err.errorDesc)
  91. } else {
  92. guard let self else { return }
  93. reloadMyProfileDetail()
  94. }
  95. }
  96. }
  97. func cleanVoiceBar(queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
  98. LNHttpManager.shared.cleanVoiceBar { [weak self] err in
  99. queue.asyncIfNotGlobal {
  100. handler(err == nil)
  101. }
  102. if let err {
  103. showToast(err.errorDesc)
  104. } else {
  105. guard let self else { return }
  106. reloadMyProfileDetail()
  107. }
  108. }
  109. }
  110. }
  111. extension LNProfileManager {
  112. func getUserProfileDetail(uid: String, queue: DispatchQueue = .main, handler: @escaping (LNUserProfileVO?) -> Void) {
  113. LNHttpManager.shared.getUsersInfo(uids: [uid]) { [weak self] res, err in
  114. guard let self else { return }
  115. if let res, err == nil {
  116. res.list.forEach {
  117. if $0.userNo.isMyUid {
  118. if self.myUserInfo.update($0) {
  119. self.notifyUserInfoChanged(newInfo: $0)
  120. }
  121. } else {
  122. self.updateProfileCache(
  123. uid: $0.userNo,
  124. name: $0.nickname,
  125. avatar: $0.avatar
  126. )
  127. self.notifyUserInfoChanged(newInfo: $0)
  128. }
  129. }
  130. } else {
  131. showToast(err?.errorDesc)
  132. }
  133. queue.asyncIfNotGlobal {
  134. handler(res?.list.first)
  135. }
  136. }
  137. }
  138. func getRandomProfile(queue: DispatchQueue = .main,
  139. handler: @escaping (LNProfileRandomInfoVO?, LNProfileRandomInfoVO?) -> Void) {
  140. if let randomProfile {
  141. handler(randomProfile.male, randomProfile.female)
  142. return
  143. }
  144. LNHttpManager.shared.getRandomProfile { [weak self] res, err in
  145. if let self, let res {
  146. randomProfile = res
  147. }
  148. queue.asyncIfNotGlobal {
  149. handler(res?.male, res?.female)
  150. }
  151. }
  152. }
  153. // func getUserOnlineState(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([String: Bool]) -> Void) {
  154. // LNHttpManager.shared.getUserOnlineState(uids: uids) { res, err in
  155. // queue.asyncIfNotGlobal {
  156. // handler(res?.list.reduce(into: [String: Bool](), { partialResult, state in
  157. // partialResult[state.userNo] = state.online
  158. // }) ?? [:])
  159. // }
  160. // }
  161. // }
  162. }
  163. extension LNProfileManager {
  164. func getCachedProfileUserInfo(uid: String) -> LNProfileUserInfo? {
  165. profileCacheLock.lock()
  166. defer { profileCacheLock.unlock() }
  167. guard let info = profileCache[uid] else {
  168. return nil
  169. }
  170. touchProfileCacheKey(uid)
  171. return info
  172. }
  173. func getCachedProfileUserInfo(uid: String,
  174. fetchIfNeeded: Bool = true,
  175. queue: DispatchQueue = .main,
  176. handler: @escaping (LNProfileUserInfo?) -> Void) {
  177. if let info = getCachedProfileUserInfo(uid: uid) {
  178. queue.asyncIfNotGlobal {
  179. handler(info)
  180. }
  181. return
  182. }
  183. guard fetchIfNeeded else {
  184. queue.asyncIfNotGlobal {
  185. handler(nil)
  186. }
  187. return
  188. }
  189. getUserProfileDetail(uid: uid, queue: queue) { [weak self] profile in
  190. guard let self, let profile else {
  191. handler(nil)
  192. return
  193. }
  194. self.updateProfileCache(
  195. uid: profile.userNo,
  196. name: profile.nickname,
  197. avatar: profile.avatar
  198. )
  199. handler(self.getCachedProfileUserInfo(uid: uid))
  200. }
  201. }
  202. func updateProfileCache(uid: String,
  203. name: String,
  204. avatar: String) {
  205. guard !uid.isEmpty else {
  206. return
  207. }
  208. profileCacheLock.lock()
  209. defer { profileCacheLock.unlock() }
  210. let info = profileCache[uid] ?? LNProfileUserInfo()
  211. info.uid = uid
  212. info.name = name
  213. info.avatar = avatar
  214. profileCache[uid] = info
  215. touchProfileCacheKey(uid)
  216. trimProfileCacheIfNeeded()
  217. }
  218. private func touchProfileCacheKey(_ uid: String) {
  219. if let index = profileCacheOrder.firstIndex(of: uid) {
  220. profileCacheOrder.remove(at: index)
  221. }
  222. profileCacheOrder.append(uid)
  223. }
  224. private func trimProfileCacheIfNeeded() {
  225. while profileCacheOrder.count > Self.profileCacheLimit {
  226. let expiredUid = profileCacheOrder.removeFirst()
  227. profileCache.removeValue(forKey: expiredUid)
  228. }
  229. }
  230. }
  231. extension LNProfileManager {
  232. func getBindPhoneCaptcha(code: String, phone: String,
  233. queue: DispatchQueue = .main,
  234. handler: @escaping (Bool) -> Void) {
  235. LNHttpManager.shared.getBindPhoneCaptcha(code: code, phone: phone) { [weak self] err in
  236. queue.asyncIfNotGlobal {
  237. handler(err == nil)
  238. }
  239. guard let self else { return }
  240. if let err {
  241. showToast(err.errorDesc)
  242. } else {
  243. captchaRemain = captchaCoolDown
  244. notifyCaptchaTime(time: captchaRemain)
  245. startCaptchaTimer()
  246. }
  247. }
  248. }
  249. func bindPhone(code: String, phone: String,
  250. queue: DispatchQueue = .main,
  251. captcha: String, handler: @escaping (Bool) -> Void) {
  252. LNHttpManager.shared.bindPhone(code: code, phone: phone, captcha: captcha) { err in
  253. queue.asyncIfNotGlobal {
  254. handler(err == nil)
  255. }
  256. if let err {
  257. showToast(err.errorDesc)
  258. }
  259. }
  260. }
  261. private func startCaptchaTimer() {
  262. DispatchQueue.main.async { [weak self] in
  263. guard let self else { return }
  264. stopCaptchaTimer()
  265. let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
  266. guard let self else { return }
  267. captchaRemain -= 1
  268. notifyCaptchaTime(time: captchaRemain)
  269. if captchaRemain == 0 {
  270. stopCaptchaTimer()
  271. }
  272. }
  273. RunLoop.main.add(timer, forMode: .common)
  274. captchaTimer = timer
  275. }
  276. }
  277. private func stopCaptchaTimer() {
  278. captchaTimer?.invalidate()
  279. captchaTimer = nil
  280. }
  281. }
  282. extension LNProfileManager {
  283. func reportCurrentLanguage(code: String,
  284. queue: DispatchQueue = .main,
  285. handler: @escaping (Bool) -> Void) {
  286. LNHttpManager.shared.reportCurrentLanguage(code: code) { err in
  287. queue.asyncIfNotGlobal {
  288. handler(err == nil)
  289. }
  290. }
  291. }
  292. }
  293. extension LNProfileManager: LNAccountManagerNotify {
  294. func onUserLogin() {
  295. reloadMyProfileDetail()
  296. }
  297. func onUserLogout() {
  298. myUserInfo = LNUserProfileVO()
  299. }
  300. }
  301. extension LNProfileManager {
  302. private func notifyUserInfoChanged(newInfo: LNUserProfileVO) {
  303. LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onUserInfoChanged(userInfo: newInfo) }
  304. }
  305. private func notifyCaptchaTime(time: Int) {
  306. LNEventDeliver.notifyEvent {
  307. ($0 as? LNProfileManagerNotify)?.onBindPhoneCaptchaCoolDownChanged(time: time)
  308. }
  309. }
  310. private func notifyUserVoiceBarInfoChanged() {
  311. LNEventDeliver.notifyEvent { ($0 as? LNProfileManagerNotify)?.onUserVoiceBarInfoChanged() }
  312. }
  313. }