瀏覽代碼

feat: 完善语音通过的逻辑

陈文艺 1 月之前
父節點
當前提交
fe8569aab0

+ 5 - 3
Lanu.xcodeproj/project.pbxproj

@@ -107,6 +107,7 @@
 				Common/Voice/LNVoiceRecorder.swift,
 				Common/Voice/LNVoiceResourceManager.swift,
 				Common/Wrapper/LNVisitedTimeWrapper.swift,
+				Files/Audio/phone_bell.mp3,
 				"Files/Font/Poppins-SemiBold.ttf",
 				Files/Svga/ic_login_gender_male_anim.svga,
 				"GoogleService-Info-Debug.plist",
@@ -126,6 +127,7 @@
 				Manager/IM/Emoji/LNEmojiData.swift,
 				Manager/IM/Emoji/LNIMEmojiManager.swift,
 				"Manager/IM/Emoji/String+TUIEmoji.swift",
+				Manager/IM/LNIMAudioCallBellPlayer.swift,
 				Manager/IM/LNIMManager.swift,
 				Manager/IM/LNIMMessageData.swift,
 				"Manager/IM/Network/LNHttpManager+IM.swift",
@@ -245,8 +247,8 @@
 				Views/IM/Chat/UserMenu/LNIMChatUserMenuView.swift,
 				Views/IM/Chat/UserMenu/LNIMChatUserRemarkPanel.swift,
 				Views/IM/Chat/ViewModel/LNIMChatViewModel.swift,
-				Views/IM/Chat/VoiceCall/LNVoiceCallFloatingView.swift,
-				Views/IM/Chat/VoiceCall/LNVoiceCallPanel.swift,
+				Views/IM/Chat/VoiceCall/LNAudioCallFloatingView.swift,
+				Views/IM/Chat/VoiceCall/LNAudioCallPanel.swift,
 				Views/IM/ConversationList/LNIMConversationCell.swift,
 				Views/IM/ConversationList/LNIMConversationListController.swift,
 				Views/IM/ConversationList/LNIMNotificationPermissionView.swift,
@@ -581,7 +583,7 @@
 				ENABLE_USER_SCRIPT_SANDBOXING = NO;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = Lanu/Info.plist;
-				INFOPLIST_KEY_CFBundleDisplayName = "Gami(Debug)";
+				INFOPLIST_KEY_CFBundleDisplayName = Gami;
 				INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
 				INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = "";
 				INFOPLIST_KEY_NSCameraUsageDescription = "need permission";

二進制
Lanu/Files/Audio/phone_bell.mp3


+ 175 - 14
Lanu/Localizable.xcstrings

@@ -3250,19 +3250,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Canceled"
+            "value" : "Call canceled by caller"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Dibatalkan"
+            "value" : "Panggilan dibatalkan"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "已取消"
+            "value" : "对方已取消"
           }
         }
       }
@@ -3733,19 +3733,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Reviews (%d)"
+            "value" : "Reviews"
           }
         },
         "id" : {
           "stringUnit" : {
-            "state" : "translated",
+            "state" : "needs_review",
             "value" : "Komentar (%d)"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "用户评论 (%d)"
+            "value" : "用户评论"
           }
         }
       }
@@ -9207,7 +9207,7 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Call Duration"
+            "value" : "Duration"
           }
         },
         "id" : {
@@ -9230,19 +9230,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Unreachable"
+            "value" : "Call timeout"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Tidak terhubung"
+            "value" : "Tidak ada jawaban"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "无法接通"
+            "value" : "超时无应答"
           }
         }
       }
@@ -9288,7 +9288,7 @@
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "对方忙"
+            "value" : "对方忙线中"
           }
         }
       }
@@ -9299,19 +9299,19 @@
         "en" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Reject Call"
+            "value" : "Call declined by user"
           }
         },
         "id" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Tolak Panggilan"
+            "value" : "Ditolak oleh penerima"
           }
         },
         "zh-Hans" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "拒绝通话"
+            "value" : "对方已拒绝"
           }
         }
       }
@@ -9384,6 +9384,167 @@
           }
         }
       }
+    },
+    "C00009" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Declined"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Ditolak"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "已拒绝"
+          }
+        }
+      }
+    },
+    "C00010" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Call wasn't answered"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tidak ada jawaban"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "对方无应答"
+          }
+        }
+      }
+    },
+    "C00011" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Line busy"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Sedang sibuk, tidak menjawab"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "忙线未接听"
+          }
+        }
+      }
+    },
+    "C00012" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Audio call"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Panggilan audio"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "语音通话"
+          }
+        }
+      }
+    },
+    "C00013" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "You have a new call"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Anda memiliki panggilan baru"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "您有一个新的通话"
+          }
+        }
+      }
+    },
+    "C00014" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "You can't call yourself"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Tidak bisa menghubungi diri sendiri"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "不能呼叫自己"
+          }
+        }
+      }
+    },
+    "C00015" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Connecting..."
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Menghubungkan…"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "正在连接…"
+          }
+        }
+      }
     }
   },
   "version" : "1.1"

+ 56 - 0
Lanu/Manager/IM/LNIMAudioCallBellPlayer.swift

@@ -0,0 +1,56 @@
+//
+//  LNIMAudioCallBellPlayer.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/2/9.
+//
+
+import Foundation
+import AVFAudio
+
+
+class LNIMAudioCallBellPlayer: NSObject {
+    private var player: AVAudioPlayer?
+    private var playing = false
+    
+    func startPlay(isInCome: Bool) {
+        if player != nil {
+            stop()
+        }
+        let path = if isInCome {
+            Bundle.main.path(forResource: "phone_bell", ofType: "mp3")
+        } else {
+            Bundle.main.path(forResource: "phone_bell", ofType: "mp3")
+        }
+        guard let path else { return }
+        
+        do {
+            player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
+        } catch let error as NSError {
+            print("Error: \(error.localizedDescription)")
+            return
+        }
+        
+        guard let prepare = player?.prepareToPlay(), prepare else {
+            return
+        }
+        
+        playing = true
+        player?.delegate = self
+        player?.play()
+    }
+    
+    func stop() {
+        playing = false
+        player?.stop()
+        player = nil
+    }
+}
+
+extension LNIMAudioCallBellPlayer: AVAudioPlayerDelegate {
+    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
+        if playing {
+            player.play()
+        }
+    }
+}

+ 70 - 36
Lanu/Manager/IM/LNIMManager.swift

@@ -101,6 +101,7 @@ class LNIMManager: NSObject {
     private(set) var voiceCallAvailable = false
     
     private(set) var curCallInfo: LNIMVoiceCallInfo?
+    private let bellPlayer = LNIMAudioCallBellPlayer()
     
     private override init() {
         super.init()
@@ -269,25 +270,41 @@ extension LNIMManager {
     }
     
     func makeVoiceCall(uid: String) {
-        guard curCallInfo == nil else { return }
-        
-        curCallInfo = .init(uid: uid)
-        curCallInfo?.isInCome = false
+        guard !uid.isMyUid else {
+            showToast(.init(key: "C00014"))
+            return
+        }
         
-        checkIfCanCall(uid: uid) { [weak self] can in
+        LNVoiceRecorder.shared.requestMicrophonePermission { [weak self] succes in
             guard let self else { return }
-            guard can else {
-                curCallInfo = nil
-                return
-            }
-            let panel = LNVoiceCallPanel()
-            panel.toCallOut(uid: uid)
-            panel.popup()
+            guard succes else { return }
+            guard curCallInfo == nil else { return }
             
-            let param = TUICallParams()
-            TUICallEngine.createInstance().call(userId: uid, callMediaType: .audio, params: param) {
-            } fail: { _, err in
-                showToast(err)
+            curCallInfo = .init(uid: uid)
+            curCallInfo?.isInCome = false
+            
+            checkIfCanCall(uid: uid) { [weak self] can in
+                guard let self else { return }
+                guard can else {
+                    curCallInfo = nil
+                    return
+                }
+                let floatingView = LNAudioCallFloatingView()
+                floatingView.show()
+                
+                let panel = LNAudioCallPanel()
+                panel.toCallOut(uid: uid)
+                panel.popup()
+                
+                let offlinePushInfo = createOfflinePushInfo()
+                let param = TUICallParams()
+                param.offlinePushInfo = offlinePushInfo
+                TUICallEngine.createInstance().call(userId: uid, callMediaType: .audio, params: param) { [weak self] in
+                    guard let self else { return }
+                    bellPlayer.startPlay(isInCome: false)
+                } fail: { _, err in
+                    showToast(err)
+                }
             }
         }
     }
@@ -356,6 +373,22 @@ extension LNIMManager {
             ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
         }
     }
+    
+    public func createOfflinePushInfo() -> TUIOfflinePushInfo {
+        let pushInfo: TUIOfflinePushInfo = TUIOfflinePushInfo()
+        pushInfo.title = ""
+        pushInfo.desc = .init(key: "C00013")
+        // iOS push type: if you want user VoIP, please modify type to TUICallIOSOfflinePushTypeVoIP
+        pushInfo.iOSPushType = .voIP
+        pushInfo.ignoreIOSBadge = false
+        pushInfo.iOSSound = "phone_bell.mp3"
+        pushInfo.androidSound = "phone_ringing"
+        // VIVO message type: 0-push message, 1-System message(have a higher delivery rate)
+        pushInfo.androidVIVOClassification = 1
+        // HuaWei message type: https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/message-classification-0000001149358835
+        pushInfo.androidHuaWeiCategory = "IM"
+        return pushInfo
+    }
 }
 
 // MARK: 语音通话变化回调 TUICallObserver
@@ -370,12 +403,18 @@ extension LNIMManager: TUICallObserver {
         curCallInfo = .init(uid: callerId)
         curCallInfo?.isInCome = true
         
-        let panel = LNVoiceCallPanel()
+        bellPlayer.startPlay(isInCome: true)
+        
+        let floatingView = LNAudioCallFloatingView()
+        floatingView.show()
+        
+        let panel = LNAudioCallPanel()
         panel.onCallIn(uid: callerId)
         panel.popup()
     }
     
     func onCallCancelled(callerId: String) {
+        bellPlayer.stop()
         curCallInfo = nil
         LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
     }
@@ -385,6 +424,7 @@ extension LNIMManager: TUICallObserver {
         LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallBegin() }
         TUICallEngine.createInstance().selectAudioPlaybackDevice(.earpiece)
         TUICallEngine.createInstance().openMicrophone { } fail: { _, _ in }
+        bellPlayer.stop()
     }
     
     func onCallEnd(roomId: TUIRoomId, callMediaType: TUICallMediaType, callRole: TUICallRole, totalTime: Float) {
@@ -392,33 +432,18 @@ extension LNIMManager: TUICallObserver {
         LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
     }
     
-    func onUserReject(userId: String) {
-        // 会同步回调 onCallCancelled
-//        curCallInfo = nil
-//        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
-    }
+    func onUserReject(userId: String) { }
     
-    func onUserNoResponse(userId: String) {
-        // 会同步回调 onCallCancelled
-//        curCallInfo = nil
-//        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
-    }
+    func onUserNoResponse(userId: String) { }
     
     func onUserLineBusy(userId: String) {
         curCallInfo = nil
         LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
+        bellPlayer.stop()
     }
 }
 
-// MARK: IM 初始化
-extension LNIMManager {
-    private func getIMSignToken(handler: @escaping (String?) -> Void) {
-        LNHttpManager.shared.getIMSign { token, err in
-            handler(token)
-        }
-    }
-}
-
+// MARK: IM 备注
 extension LNIMManager {
     func setUserRemark(uid: String, remark: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
         LNHttpManager.shared.setUserRemark(uid: uid, remark: remark) { [weak self] err in
@@ -444,6 +469,15 @@ extension LNIMManager {
     }
 }
 
+// MARK: IM 初始化
+extension LNIMManager {
+    private func getIMSignToken(handler: @escaping (String?) -> Void) {
+        LNHttpManager.shared.getIMSign { token, err in
+            handler(token)
+        }
+    }
+}
+
 extension LNIMManager: LNAccountManagerNotify {
     func onUserLogin() {
         // 初始化 SDK

+ 38 - 0
Lanu/Manager/IM/LNIMMessageData.swift

@@ -42,6 +42,7 @@ class LNIMVoiceCallMessage: Decodable {
     var data: String = ""
     
     private var decodedData: LNIMVoiceCallData?
+    var isSelf = false
     
     var callData: LNIMVoiceCallData? {
         if let decodedData {
@@ -62,6 +63,43 @@ class LNIMVoiceCallMessage: Decodable {
         || actionType == .reject_Invite
         || actionType == .invite_Timeout
     }
+    
+    var contentDesc: String {
+        guard let callData else { return "" }
+        
+        return if actionType == SignalingActionType.invite,
+           let data = callData.data, data.cmd == "hangup" {
+            .init(key: "C00001") + " " + callData.call_end.timeCountDisplay
+        } else if actionType == SignalingActionType.cancel_Invite {
+            if isSelf {
+                .init(key: "A00152")
+            } else {
+                .init(key: "A00142")
+            }
+        } else if actionType == SignalingActionType.reject_Invite {
+            if callData.data?.cmd == "line_busy" {
+                if isSelf {
+                    .init(key: "C00004")
+                } else {
+                    .init(key: "C00011")
+                }
+            } else {
+                if isSelf {
+                    .init(key: "C00005")
+                } else {
+                    .init(key: "C00009")
+                }
+            }
+        } else if actionType == SignalingActionType.invite_Timeout {
+            if isSelf {
+                .init(key: "C00010")
+            } else {
+                .init(key: "C00002")
+            }
+        } else {
+            ""
+        }
+    }
 }
 
 @AutoCodable

+ 7 - 1
Lanu/Manager/IM/TUIUtils/V2TIMConversation+Extension.swift

@@ -37,9 +37,15 @@ extension V2TIMConversation {
             if let lastMessage {
                 let imMessage = LNIMMessageData(imMessage: lastMessage)
                 if case .official = imMessage.type {
-                    attrString.append(.init(string: .init(key: "A00019")))
+                    if let message: LNIMOfficialMessage = imMessage.decodeCustomMessage() {
+                        attrString.append(.init(string: message.title))
+                    } else {
+                        attrString.append(.init(string: .init(key: "A00019")))
+                    }
                 } else if case .order = imMessage.type {
                     attrString.append(.init(string: .init(key: "A00020")))
+                } else if case .call = imMessage.type {
+                    attrString.append(.init(string: .init(key: "C00012")))
                 } else {
                     let lastMsgStr = lastMessage.displayString
                     guard let lastMsgStr else { return nil }

+ 12 - 2
Lanu/Views/Game/Skill/LNSkillCommentsView.swift

@@ -12,7 +12,7 @@ import SnapKit
 
 class LNSkillCommentsView: UIView {
     private let starLabel = UILabel()
-    private let titleLabel = UILabel()
+    private let countLabel = UILabel()
     private let stackView = UIStackView()
     
     private var curSkill: LNGameMateSkillDetailVO?
@@ -31,7 +31,7 @@ class LNSkillCommentsView: UIView {
             guard let self else { return }
             guard let res else { return }
             isHidden = res.list.isEmpty
-            titleLabel.text = .init(key: "A00163", res.total)
+            countLabel.text = "(\(res.total))"
             reloadList(res.list)
         }
     }
@@ -118,6 +118,8 @@ extension LNSkillCommentsView {
             make.width.height.equalTo(4)
         }
         
+        let titleLabel = UILabel()
+        titleLabel.text = .init(key: "A00163")
         titleLabel.font = .heading_h3
         titleLabel.textColor = .text_5
         container.addSubview(titleLabel)
@@ -126,6 +128,14 @@ extension LNSkillCommentsView {
             make.leading.equalTo(dotView.snp.trailing).offset(8)
         }
         
+        countLabel.font = .heading_h4
+        countLabel.textColor = .text_5
+        container.addSubview(countLabel)
+        countLabel.snp.makeConstraints { make in
+            make.leading.equalTo(titleLabel.snp.trailing)
+            make.centerY.equalTo(titleLabel)
+        }
+        
         let arrow = UIImageView.arrowImageView(size: 10)
         arrow.tintColor = .text_4
         container.addSubview(arrow)

+ 1 - 18
Lanu/Views/IM/Chat/Cells/LNIMChatCallMessageCell.swift

@@ -18,24 +18,7 @@ class LNIMChatCallMessageCell: LNIMChatBaseMessageCell {
         super.update(data, viewModel: viewModel)
         
         guard let order: LNIMVoiceCallMessage = data.decodeCustomMessage() else { return }
-        guard let callData = order.callData else { return }
-        
-        if order.actionType == SignalingActionType.invite,
-           let data = callData.data, data.cmd == "hangup" {
-            contextLabel.text = .init(key: "C00001") + " " + callData.call_end.timeCountDisplay
-        } else if order.actionType == SignalingActionType.cancel_Invite {
-            contextLabel.text = .init(key: "C00003")
-        } else if order.actionType == SignalingActionType.reject_Invite {
-            if callData.data?.cmd == "line_busy" {
-                contextLabel.text = .init(key: "C00004")
-            } else {
-                contextLabel.text = .init(key: "C00005")
-            }
-        } else if order.actionType == SignalingActionType.invite_Timeout {
-            contextLabel.text = .init(key: "C00002")
-        } else {
-            contextLabel.text = ""
-        }
+        contextLabel.text = order.contentDesc
     }
     
     override func setupViews() {

+ 4 - 1
Lanu/Views/IM/Chat/ViewModel/LNIMChatViewModel.swift

@@ -209,10 +209,12 @@ extension LNIMChatViewModel {
         allMessage.append(contentsOf: datas)
         notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true)
         
+        let pushInfo = V2TIMOfflinePushInfo()
+        pushInfo.title = myUserInfo.nickname
         V2TIMManager.sharedInstance().sendMessage(
             message: message, receiver: userId,
             groupID: "", priority: .PRIORITY_NORMAL,
-            onlineUserOnly: false, offlinePushInfo: nil,
+            onlineUserOnly: false, offlinePushInfo: pushInfo,
             progress: nil)
         { [weak self] in
             guard let self else { return }
@@ -321,6 +323,7 @@ extension LNIMChatViewModel {
                 guard let callMessage: LNIMVoiceCallMessage = data.decodeCustomMessage() else {
                     continue // 解析失败,忽略
                 }
+                callMessage.isSelf = message.isSelf
                 // 被标记不展示
                 if !callMessage.showOnList {
                     continue

+ 26 - 23
Lanu/Views/IM/Chat/VoiceCall/LNVoiceCallFloatingView.swift → Lanu/Views/IM/Chat/VoiceCall/LNAudioCallFloatingView.swift

@@ -1,5 +1,5 @@
 //
-//  LNVoiceCallFloatingView.swift
+//  LNAudioCallFloatingView.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/2/3.
@@ -10,7 +10,9 @@ import UIKit
 import SnapKit
 
 
-class LNVoiceCallFloatingView: UIView {
+class LNAudioCallFloatingView: UIView {
+    private let size: CGFloat = 67
+    
     private let stackView = UIStackView()
     private let stateIc = UIImageView()
     private let durationLabel = UILabel()
@@ -33,11 +35,12 @@ class LNVoiceCallFloatingView: UIView {
     }
     
     func show() {
-        UIView.appKeyWindow?.addSubview(self)
-        snp.makeConstraints { make in
-            make.trailing.equalToSuperview().offset(-16)
-            make.bottom.equalToSuperview().offset(-100)
-        }
+        guard let window = UIView.appKeyWindow else { return }
+        window.addSubview(self)
+        
+        frame = .init(x: window.bounds.width - 16 - size,
+                      y: window.bounds.height - 100.0 - size,
+                      width: size, height: size)
     }
     
     required init?(coder: NSCoder) {
@@ -45,7 +48,7 @@ class LNVoiceCallFloatingView: UIView {
     }
 }
 
-extension LNVoiceCallFloatingView {
+extension LNAudioCallFloatingView {
     private func dismiss() {
         removeFromSuperview()
     }
@@ -78,7 +81,7 @@ extension LNVoiceCallFloatingView {
     }
 }
 
-extension LNVoiceCallFloatingView {
+extension LNAudioCallFloatingView {
     @objc
     private func handlePan(_ ges: UIPanGestureRecognizer) {
         let location = ges.location(in: superview)
@@ -96,18 +99,18 @@ extension LNVoiceCallFloatingView {
     }
 }
 
-extension LNVoiceCallFloatingView {
+extension LNAudioCallFloatingView {
     private func updatePosition(animated: Bool) {
         guard let superview else { return }
         let movement = { [weak self] in
             guard let self else { return }
             
             let y = center.y.bounded(min: 160, max: superview.bounds.height - 160)
-            if center.x > superview.bounds.width * 0.5 {
+//            if center.x > superview.bounds.width * 0.5 {
                 center = .init(x: superview.bounds.width - bounds.width * 0.5 - 16, y: y)
-            } else {
-                center = .init(x: bounds.width * 0.5 + 16, y: y)
-            }
+//            } else {
+//                center = .init(x: bounds.width * 0.5 + 16, y: y)
+//            }
         }
         if animated {
             UIView.animate(withDuration: 0.25, animations: movement)
@@ -117,11 +120,14 @@ extension LNVoiceCallFloatingView {
     }
     
     private func setupViews() {
+        layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
+        layer.shadowOffset = .init(width: 0, height: 4)
+        layer.shadowRadius = 6
+        layer.shadowOpacity = 0.5
+        layer.masksToBounds = false
+        
         backgroundColor = .fill
         layer.cornerRadius = 12
-        snp.makeConstraints { make in
-            make.width.height.equalTo(67)
-        }
         
         stackView.axis = .vertical
         stackView.spacing = 8
@@ -144,18 +150,15 @@ extension LNVoiceCallFloatingView {
         let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
         addGestureRecognizer(pan)
         
-        onTap { [weak self] in
-            guard let self else { return }
-            dismiss()
-            
-            let panel = LNVoiceCallPanel()
+        onTap {
+            let panel = LNAudioCallPanel()
             panel.resume()
             panel.popup()
         }
     }
 }
 
-extension LNVoiceCallFloatingView: LNIMManagerNotify {
+extension LNAudioCallFloatingView: LNIMManagerNotify {
     func onVoiceCallEnd() {
         dismiss()
     }

+ 32 - 25
Lanu/Views/IM/Chat/VoiceCall/LNVoiceCallPanel.swift → Lanu/Views/IM/Chat/VoiceCall/LNAudioCallPanel.swift

@@ -1,5 +1,5 @@
 //
-//  LNVoiceCallPanel.swift
+//  LNAudioCallPanel.swift
 //  Gami
 //
 //  Created by OneeChan on 2026/2/1.
@@ -10,7 +10,7 @@ import UIKit
 import SnapKit
 
 
-class LNVoiceCallPanel: LNPopupView {
+class LNAudioCallPanel: LNPopupView {
     private let background = UIImageView()
     
     private let avatar = UIImageView()
@@ -44,14 +44,14 @@ class LNVoiceCallPanel: LNPopupView {
     
     func toCallOut(uid: String) {
         callOutView.isHidden = false
+        stateLabel.isHidden = false
         reloadUserInfo(uid: uid)
-        getCurOrders(uid: uid)
     }
     
     func onCallIn(uid: String) {
         onCallView.isHidden = false
+        stateLabel.isHidden = false
         reloadUserInfo(uid: uid)
-        getCurOrders(uid: uid)
     }
     
     func resume() {
@@ -60,14 +60,17 @@ class LNVoiceCallPanel: LNPopupView {
             updateCallDuration()
             startTimer()
             callingView.isHidden = false
+            stateLabel.isHidden = true
+            getCurOrders(uid: callInfo.uid)
         } else if callInfo.isInCome {
             onCallView.isHidden = false
+            stateLabel.isHidden = false
         } else {
             callOutView.isHidden = false
+            stateLabel.isHidden = false
         }
         
         reloadUserInfo(uid: callInfo.uid)
-        getCurOrders(uid: callInfo.uid)
     }
     
     required init?(coder: NSCoder) {
@@ -75,7 +78,7 @@ class LNVoiceCallPanel: LNPopupView {
     }
 }
 
-extension LNVoiceCallPanel {
+extension LNAudioCallPanel {
     private func reloadUserInfo(uid: String) {
         LNProfileManager.shared.getUserProfile(uid: uid) { [weak self] info in
             guard let self else { return }
@@ -97,7 +100,7 @@ extension LNVoiceCallPanel {
             orderView.isHidden = false
             gameIc.sd_setImage(with: URL(string: order.categoryIcon))
             gameNameLabel.text = order.bizCategoryName
-            orderTimeLabel.text = Double(order.createTime / 1_000).tencentIMTimeDesc
+            orderTimeLabel.text = Double(order.createTime / 1_000).formattedFullDateWithTime()
             gameCountLabel.text = "x \(order.purchaseQty) \(order.unit)"
             orderStateLabel.text = order.statusUI.title
         }
@@ -130,14 +133,19 @@ extension LNVoiceCallPanel {
     }
 }
 
-extension LNVoiceCallPanel: LNIMManagerNotify {
+extension LNAudioCallPanel: LNIMManagerNotify {
     func onVoiceCallBegin() {
         onCallView.isHidden = true
         callOutView.isHidden = true
         callingView.isHidden = false
+        stateLabel.isHidden = true
         
         updateCallDuration()
         startTimer()
+        
+        if let callInfo = LNIMManager.shared.curCallInfo {
+            getCurOrders(uid: callInfo.uid)
+        }
     }
     
     func onVoiceCallEnd() {
@@ -148,8 +156,7 @@ extension LNVoiceCallPanel: LNIMManagerNotify {
     func onVoiceCallInfoChanged() {
         guard let callInfo = LNIMManager.shared.curCallInfo else { return }
         muteButton.setImage(callInfo.isMute ? .icCallMute : .icCallUnmute, for: .normal)
-        let icon: UIImage = if !DevicesUtil.isBluetoothHeadsetConnected,
-                               callInfo.deviceType == .earpiece {
+        let icon: UIImage = if callInfo.deviceType == .earpiece {
             .icCallSpeakerEarpiece
         } else {
             .icCallSpeakerPhone
@@ -158,7 +165,7 @@ extension LNVoiceCallPanel: LNIMManagerNotify {
     }
 }
 
-extension LNVoiceCallPanel {
+extension LNAudioCallPanel {
     private func setupViews() {
         containerHeight = .percent(1.0)
         
@@ -183,6 +190,7 @@ extension LNVoiceCallPanel {
             make.bottom.equalTo(container.snp.centerY).offset(-30).priority(.medium)
         }
         
+        stateLabel.text = .init(key: "C00015")
         stateLabel.font = .body_xl
         stateLabel.textColor = .text_2
         container.addSubview(stateLabel)
@@ -226,7 +234,7 @@ extension LNVoiceCallPanel {
         
 //         可选:添加半透明遮罩,增强模糊层次感(毛玻璃常用搭配)
         let maskView = UIView(frame: blurView.bounds)
-        maskView.backgroundColor = UIColor.black.withAlphaComponent(0.42) // 0.1~0.3为宜
+        maskView.backgroundColor = UIColor.black.withAlphaComponent(0.3) // 0.1~0.3为宜
         blurView.contentView.addSubview(maskView)
         maskView.snp.makeConstraints { make in
             make.edges.equalToSuperview()
@@ -243,8 +251,6 @@ extension LNVoiceCallPanel {
         minButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             dismiss()
-            let floatingView = LNVoiceCallFloatingView()
-            floatingView.show()
         }), for: .touchUpInside)
         navBar.actionView.addSubview(minButton)
         minButton.snp.makeConstraints { make in
@@ -438,17 +444,18 @@ extension LNVoiceCallPanel {
         speakerButton.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
 //            if DevicesUtil.isBluetoothHeadsetConnected {
-                let menu = LNVoiceCallSpeakerSelectPopoverMenu()
-                menu.pointAt(parentView: self, targetView: speakerButton) { type in
-                    LNIMManager.shared.switchVoiceCallSpeakerType(type: type)
-                }
-//            } else if let callInfo = LNIMManager.shared.curCallInfo {
-//                if callInfo.deviceType == .speakerphone {
-//                    LNIMManager.shared.switchVoiceCallSpeakerType(type: .earpiece)
-//                } else {
-//                    LNIMManager.shared.switchVoiceCallSpeakerType(type: .speakerphone)
+//                let menu = LNVoiceCallSpeakerSelectPopoverMenu()
+//                menu.pointAt(parentView: self, targetView: speakerButton) { type in
+//                    LNIMManager.shared.switchVoiceCallSpeakerType(type: type)
 //                }
-//            }
+//            } else
+            if let callInfo = LNIMManager.shared.curCallInfo {
+                if callInfo.deviceType == .speakerphone {
+                    LNIMManager.shared.switchVoiceCallSpeakerType(type: .earpiece)
+                } else {
+                    LNIMManager.shared.switchVoiceCallSpeakerType(type: .speakerphone)
+                }
+            }
         }), for: .touchUpInside)
         stackView.addArrangedSubview(speakerButton)
         
@@ -601,7 +608,7 @@ struct LNVoiceCallPanelPreview: UIViewRepresentable {
         let container = UIView()
         container.backgroundColor = .lightGray
         
-        let view = LNVoiceCallPanel()
+        let view = LNAudioCallPanel()
         view.popup()
         
         return container

+ 1 - 1
Lanu/Views/Order/Detail/LNOrderDetailViewController.swift

@@ -181,7 +181,7 @@ extension LNOrderDetailViewController {
         case .cancelled:
             backgroundIc.image = .icOrderCancelledBg
             stateIc.image = .icOrderStatusCancel
-            stateLabel.text = .init(key: "A00142")
+            stateLabel.text = .init(key: "A00152")
         }
     }
     

+ 0 - 2
Podfile

@@ -11,9 +11,7 @@ target 'Gami' do
   pod 'TIMCommon', :path => "./ThirdParty/TUIKit/TIMCommon"
   pod 'TUIChat', :path => "./ThirdParty/TUIKit/TUIChat"
   pod 'TIMPush'
-  
   pod 'TUICallEngine'
-  pod 'TIMPush'
   
   pod 'DoraemonKit', :configurations => ['Debug']
   

+ 6 - 6
Podfile.lock

@@ -37,16 +37,16 @@ PODS:
   - TUIChat (1.0.0):
     - TIMCommon
   - TXIMSDK_Plus_iOS_XCFramework (8.7.7201)
-  - TXLiteAVSDK_TRTC (12.8.19666):
-    - TXLiteAVSDK_TRTC/TRTC (= 12.8.19666)
-  - TXLiteAVSDK_TRTC/TRTC (12.8.19666)
+  - TXLiteAVSDK_TRTC (13.1.20454):
+    - TXLiteAVSDK_TRTC/TRTC (= 13.1.20454)
+  - TXLiteAVSDK_TRTC/TRTC (13.1.20454)
 
 DEPENDENCIES:
   - Adjust
   - DoraemonKit
   - TIMCommon (from `./ThirdParty/TUIKit/TIMCommon`)
   - TIMPush
-  - TUICallEngine
+  - TUICallEngine (>= 2.7.0.1151)
   - TUIChat (from `./ThirdParty/TUIKit/TUIChat`)
 
 SPEC REPOS:
@@ -78,8 +78,8 @@ SPEC CHECKSUMS:
   TUICallEngine: f00a90ab800d6008c253bb2fc6200cd21ee1133a
   TUIChat: 696bca6e2a6cfd2bc22f624425b425b68bd9506c
   TXIMSDK_Plus_iOS_XCFramework: 3b435eae84c639f35ae8dc9c8b92c399a8b0a67f
-  TXLiteAVSDK_TRTC: b576b0c6a477fa98b5d2b33be63fa9aa7c41f0eb
+  TXLiteAVSDK_TRTC: 3d4310a5976977448568f5cc605a40b2219c4c43
 
-PODFILE CHECKSUM: 86f86efa83be453f2392b4fadfc195ca9ac216c8
+PODFILE CHECKSUM: e5888dd10ace991d47e72fdee25573383b5af246
 
 COCOAPODS: 1.16.2