464 lines
14 KiB
Swift
464 lines
14 KiB
Swift
|
|
|
|||
|
|
import NIMSDK
|
|||
|
|
import SnapKit
|
|||
|
|
import TZImagePickerController
|
|||
|
|
import UIKit
|
|||
|
|
import Combine
|
|||
|
|
class SessionController: CLBaseViewController {
|
|||
|
|
var sessionNavigationView: SessionNavigationView!
|
|||
|
|
var bgImageView: UIImageView!
|
|||
|
|
var overlay: GradientView!
|
|||
|
|
|
|||
|
|
var tableView: UITableView!
|
|||
|
|
// var headView: SessionAIHeadView!
|
|||
|
|
|
|||
|
|
// MARK: BottomViews
|
|||
|
|
var bottomViewsStackV : UIStackView!
|
|||
|
|
var inputEntrance: SessionInputOperateView!
|
|||
|
|
var inputBar: SessionInputView!
|
|||
|
|
var moreView: IMMoreItemView!
|
|||
|
|
var giftSendView: GiftGridSendView!
|
|||
|
|
var textSuggestionsView : SessionInputSuggestionsView!
|
|||
|
|
|
|||
|
|
var voiceHoldView: IMVoiceHoldView!
|
|||
|
|
var pureBgOperateView:SessionPureBgOperateView!
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 长按菜单响应的cell
|
|||
|
|
var menuCell: SessionCell?
|
|||
|
|
|
|||
|
|
// IM user info
|
|||
|
|
var aiInfo: IMAIUserInfo?
|
|||
|
|
|
|||
|
|
// -- Layout
|
|||
|
|
var bottomConstraintForInputBar: Constraint?
|
|||
|
|
|
|||
|
|
// -- flag
|
|||
|
|
var firstLoadedUser = false;
|
|||
|
|
var isDisappearing = false// 是否正在消失
|
|||
|
|
var dealCancelEditing = false// 是否正在退出键盘输入
|
|||
|
|
var kuolieReportMatchedChatOnce = false // v1.6.0 kuolie
|
|||
|
|
var isRequesting = false
|
|||
|
|
var isPullingToAIHomePage = false
|
|||
|
|
|
|||
|
|
// --- view model
|
|||
|
|
lazy var giftVM = GiftViewModel()
|
|||
|
|
|
|||
|
|
/// 此版本也是aiId
|
|||
|
|
var aiId: Int!
|
|||
|
|
/// 439257063882753@r@t
|
|||
|
|
var accountId:String!
|
|||
|
|
/// eg: 439213911113729@u@t|1|439257063882753@r@t
|
|||
|
|
var conversationId: String!
|
|||
|
|
var conversation: V2NIMConversation!
|
|||
|
|
|
|||
|
|
var util: SessionUtil! = SessionUtil()
|
|||
|
|
|
|||
|
|
var cancellables = Set<AnyCancellable>()
|
|||
|
|
|
|||
|
|
convenience init(accountID: String) {
|
|||
|
|
self.init()
|
|||
|
|
accountId = accountID
|
|||
|
|
|
|||
|
|
let array = accountID.components(separatedBy: "@")
|
|||
|
|
if let number = Int(array.first ?? "0"){
|
|||
|
|
aiId = number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
self.conversationId = V2NIMConversationIdUtil.p2pConversationId(accountID)
|
|||
|
|
if self.conversationId != nil{
|
|||
|
|
conversation = V2NIMConversation()
|
|||
|
|
NIMSDK.shared().v2ConversationService.getConversation(conversationId) {[weak self] conversation in
|
|||
|
|
self?.conversation = conversation
|
|||
|
|
} failure: { error in
|
|||
|
|
dlog("☁️❌get \(String(describing: self.conversationId)) conversation error:\(error)")
|
|||
|
|
}
|
|||
|
|
}else{
|
|||
|
|
dlog("❌p2pConversationId failed")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
convenience init(conversationId: String) {
|
|||
|
|
self.init()
|
|||
|
|
self.conversationId = conversationId
|
|||
|
|
conversation = V2NIMConversation()
|
|||
|
|
|
|||
|
|
let stings = conversationId.components(separatedBy: "|")
|
|||
|
|
guard let last = stings.last else{return}
|
|||
|
|
accountId = last
|
|||
|
|
let strings2 = last.components(separatedBy:"@")
|
|||
|
|
if let userIdStr = strings2.first {
|
|||
|
|
if let userid = Int(userIdStr) {
|
|||
|
|
aiId = userid
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
NIMSDK.shared().v2ConversationService.getConversation(conversationId) {[weak self] conversation in
|
|||
|
|
self?.conversation = conversation
|
|||
|
|
} failure: { error in
|
|||
|
|
dlog("☁️❌get \(String(describing: self.conversationId)) conversation error:\(error)")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewDidLoad() {
|
|||
|
|
super.viewDidLoad()
|
|||
|
|
setupUI()
|
|||
|
|
setupData()
|
|||
|
|
setupEvent()
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewDidAppear(_ animated: Bool) {
|
|||
|
|
super.viewDidAppear(animated)
|
|||
|
|
(navigationController as? CLNavigationController)?.disabledFullScreenPan()
|
|||
|
|
IMAIViewModel.shared.updateAI(aiInfo)
|
|||
|
|
IMManager.shared.addCache(sessionID: conversationId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewWillAppear(_ animated: Bool) {
|
|||
|
|
super.viewWillAppear(animated)
|
|||
|
|
isDisappearing = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|||
|
|
super.viewWillDisappear(animated)
|
|||
|
|
isDisappearing = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|||
|
|
super.viewDidDisappear(animated)
|
|||
|
|
isDisappearing = false
|
|||
|
|
(navigationController as? CLNavigationController)?.enabledFullScreenPan()
|
|||
|
|
cancelEditing()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
override func viewDidLayoutSubviews() {
|
|||
|
|
super.viewDidLayoutSubviews()
|
|||
|
|
|
|||
|
|
// 动态调整tableView的contentInset
|
|||
|
|
adjustTableViewContentInset()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
deinit {
|
|||
|
|
IMManager.shared.deleteCache(sessionID: conversationId)
|
|||
|
|
IMManager.shared.clearUnreadCountBy(ids: [conversationId])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Views
|
|||
|
|
|
|||
|
|
extension SessionController {
|
|||
|
|
func setupUI() {
|
|||
|
|
view.clipsToBounds = true
|
|||
|
|
navigationView.backgroundColor = .clear
|
|||
|
|
|
|||
|
|
sessionNavigationView = {
|
|||
|
|
let v = SessionNavigationView()
|
|||
|
|
view.addSubview(v)
|
|||
|
|
v.snp.makeConstraints { make in
|
|||
|
|
make.top.leading.trailing.equalToSuperview()
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
bgImageView = {
|
|||
|
|
let v = UIImageView()
|
|||
|
|
v.contentMode = .scaleAspectFill
|
|||
|
|
view.addSubview(v)
|
|||
|
|
v.snp.makeConstraints { make in
|
|||
|
|
make.leading.trailing.equalToSuperview()
|
|||
|
|
make.top.equalToSuperview()
|
|||
|
|
make.bottom.equalToSuperview() // 得加,不然图片高度不够撑满
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}()
|
|||
|
|
// bgImageView.image = UIImage(named: "egpic")?.cropImageTop(with: 1 / UIScreen.aspectRatio)
|
|||
|
|
|
|||
|
|
overlay = {
|
|||
|
|
let v = GradientView(colors: [UIColor.c.cbn.withAlphaComponent(1), UIColor.c.cbn.withAlphaComponent(0), UIColor.c.cbn.withAlphaComponent(0), UIColor.c.cbn.withAlphaComponent(1)], gradientType: .topToBottom)
|
|||
|
|
v.imBgOverlayMode = true
|
|||
|
|
view.addSubview(v)
|
|||
|
|
v.snp.makeConstraints { make in
|
|||
|
|
make.edges.equalToSuperview()
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}()
|
|||
|
|
|
|||
|
|
setupInputView()
|
|||
|
|
|
|||
|
|
|
|||
|
|
setupTableView()
|
|||
|
|
|
|||
|
|
view.bringSubviewToFront(sessionNavigationView)
|
|||
|
|
view.bringSubviewToFront(bottomViewsStackV)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func setupUserInfo() {
|
|||
|
|
|
|||
|
|
let imUserInfo = IMUserKit.imUserKitWith(accId: accountId) {[weak self] kit in
|
|||
|
|
if self?.conversation.conversationId == kit?.accountId{
|
|||
|
|
self?.navigationView.titleLabel.text = kit?.nickname
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if let userId = imUserInfo.userId, userId > 0 {
|
|||
|
|
// titleView.titleLabel.text = kitInfo.showName
|
|||
|
|
Hud.showIndicator()
|
|||
|
|
requestUserInfo(userId: userId)
|
|||
|
|
} else if aiId > 0 {
|
|||
|
|
Hud.showIndicator()
|
|||
|
|
requestUserInfo(userId: aiId)
|
|||
|
|
}else{
|
|||
|
|
assert(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func reloadViews() {
|
|||
|
|
guard let user = aiInfo else {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bgImageView.loadImage(user.backgroundImg, completionBlock: {[weak self] result in
|
|||
|
|
switch result {
|
|||
|
|
case let .success(imageResult):
|
|||
|
|
let image = imageResult.image
|
|||
|
|
self?.bgImageView.snp.makeConstraints { make in
|
|||
|
|
make.width.equalTo((self?.bgImageView.snp.height)!).multipliedBy(image.size.width / image.size.height)
|
|||
|
|
}
|
|||
|
|
//self?.bgImageView.image = image.cropImageTop(with: 1/UIScreen.aspectRatio)
|
|||
|
|
case .failure:
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
sessionNavigationView.config(user: user)
|
|||
|
|
|
|||
|
|
tableView.reloadData()
|
|||
|
|
// 刷新一些页面上的其他数据(通过UserInfo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: Datas & Events
|
|||
|
|
|
|||
|
|
extension SessionController {
|
|||
|
|
func setupData() {
|
|||
|
|
setupNIM()
|
|||
|
|
|
|||
|
|
util.conversationId = conversationId
|
|||
|
|
|
|||
|
|
setupUserInfo()
|
|||
|
|
|
|||
|
|
WalletCore.shared.refreshWallet()
|
|||
|
|
// 进入页面需要标记已读
|
|||
|
|
markReadAll()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Noti
|
|||
|
|
|
|||
|
|
extension SessionController {
|
|||
|
|
@objc func keyboardWillChanged(noti: Notification) {
|
|||
|
|
// guard tableView.window != nil else {
|
|||
|
|
// return
|
|||
|
|
// }
|
|||
|
|
if isDisappearing || isDisplaying == false {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
guard let userInfo = noti.userInfo else { return }
|
|||
|
|
|
|||
|
|
// 获取参数
|
|||
|
|
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
|
|||
|
|
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey]! as! Int
|
|||
|
|
let beginFrame = (userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
|
|||
|
|
let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
|
|||
|
|
|
|||
|
|
var hideKeyboardFlag = false
|
|||
|
|
var showKeyboardFlag = false
|
|||
|
|
|
|||
|
|
// dlog("⌨️duration:\(duration)")
|
|||
|
|
|
|||
|
|
if beginFrame.origin.y < UIScreen.height, endFrame.origin.y >= UIScreen.height {
|
|||
|
|
// 收起键盘
|
|||
|
|
hideKeyboardFlag = true
|
|||
|
|
//dlog("⌨️close keyboard")
|
|||
|
|
} else if beginFrame.origin.y >= UIScreen.height, endFrame.origin.y < UIScreen.height {
|
|||
|
|
// 弹出键盘
|
|||
|
|
showKeyboardFlag = true
|
|||
|
|
//dlog("⌨️show keyboard")
|
|||
|
|
} else if beginFrame.size.height != endFrame.size.height {
|
|||
|
|
// 键盘高度变更
|
|||
|
|
//dlog("⌨️height of keyboard changed")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
if hideKeyboardFlag{ // 将要关键盘
|
|||
|
|
self.doHideKeyBordActions()
|
|||
|
|
}else if showKeyboardFlag{ // 将要显示键盘
|
|||
|
|
self.doKeyboardShowActions()
|
|||
|
|
let offsetY = UIScreen.height - endFrame.origin.y// - UIWindow.safeAreaBottom
|
|||
|
|
self.updateInputEntrance(offsetY: offsetY, duration: duration, curve: curve)
|
|||
|
|
}
|
|||
|
|
else if !self.dealCancelEditing {
|
|||
|
|
let offsetY = UIScreen.height - endFrame.origin.y// - UIWindow.safeAreaBottom
|
|||
|
|
self.updateInputEntrance(offsetY: offsetY, duration: duration, curve: curve)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
UIView.animate(withDuration: duration,
|
|||
|
|
delay: 0,
|
|||
|
|
options: UIView.AnimationOptions(rawValue: UInt(curve) << 16),
|
|||
|
|
animations: {
|
|||
|
|
self.view.layoutIfNeeded()
|
|||
|
|
|
|||
|
|
self.scrollToBottom(self.tableView, animated: true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func menuHide(noti: Notification) {
|
|||
|
|
guard let menu = noti.object as? UIMenuController else { return }
|
|||
|
|
guard let menuItems = menu.menuItems else { return }
|
|||
|
|
guard let first = menuItems.first else { return }
|
|||
|
|
if first.action == Selector(("copyAction")) {
|
|||
|
|
UIMenuController.shared.menuItems = nil
|
|||
|
|
} else {
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func notifyChatSettingUpdated(){
|
|||
|
|
requestUserInfo(userId: aiId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func notifiyRelationHiddenUpdate(){
|
|||
|
|
requestUserInfo(userId: aiId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func notifiyRelationInfoUpdate(){
|
|||
|
|
requestUserInfo(userId: aiId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Action
|
|||
|
|
|
|||
|
|
// BottomViews
|
|||
|
|
extension SessionController {
|
|||
|
|
@objc func titleTapAction() {
|
|||
|
|
guard let user = aiInfo, let aiId = user.aiId else { return }
|
|||
|
|
AppRouter.goAIRoleHome(aiId: aiId)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
/// 主动退出键盘
|
|||
|
|
@objc func cancelEditing() {
|
|||
|
|
dealCancelEditing = true
|
|||
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|||
|
|
self.dealCancelEditing = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if inputBar.textView.isFirstResponder{
|
|||
|
|
view.endEditing(true)
|
|||
|
|
}else{
|
|||
|
|
hideOperateView()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func doKeyboardShowActions(){
|
|||
|
|
self.inputBar.isHidden = false
|
|||
|
|
|
|||
|
|
self.inputEntrance.isHidden = true
|
|||
|
|
// self.moreView.isHidden = true
|
|||
|
|
//showMoreItems(show: false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@objc func doHideKeyBordActions() {
|
|||
|
|
// showHeaderView(show: true, duration: 0.3)
|
|||
|
|
|
|||
|
|
UIView.animate(withDuration: 0.1) {
|
|||
|
|
self.inputBar.alpha = 0
|
|||
|
|
self.inputBar.layoutIfNeeded()
|
|||
|
|
} completion: { finished in
|
|||
|
|
self.inputBar.isHidden = true
|
|||
|
|
self.inputBar.alpha = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// self.inputEntrance.isHidden = false
|
|||
|
|
// self.bottomViewsStackV.setNeedsDisplay()
|
|||
|
|
// self.bottomViewsStackV.layoutIfNeeded()
|
|||
|
|
// showMoreItems(show: false)
|
|||
|
|
hideAllBottomViews(except: [inputEntrance])
|
|||
|
|
updateInputEntrance(offsetY: 0, duration: 0, curve: UIView.AnimationCurve.easeOut.rawValue) // 0.3
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// MARK: - Session Audio Phone call delegaet.
|
|||
|
|
|
|||
|
|
//
|
|||
|
|
// extension SessionController: SessionHeadPhoneCallGuideDelegate {
|
|||
|
|
// func headPhoneCallGuideTapCall() {
|
|||
|
|
// guard AudioRecordTool.audioAuth() else {
|
|||
|
|
// return
|
|||
|
|
// }
|
|||
|
|
//
|
|||
|
|
// guard AudioPlayTool.audioChannelFreeToUse(), PhoneManager.isInPhoneChannel() == false else {
|
|||
|
|
// return
|
|||
|
|
// }
|
|||
|
|
//
|
|||
|
|
// hideCallGuide()
|
|||
|
|
// phoneCallHandler.onSelectVoice(user: userInfo, session: session)
|
|||
|
|
// }
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
// MARK: - Animation
|
|||
|
|
|
|||
|
|
extension SessionController {
|
|||
|
|
// func showMoreItem(show: Bool) {
|
|||
|
|
// if show {
|
|||
|
|
// UIView.animate(withDuration: 0.3) {
|
|||
|
|
// self.moreView.alpha = 1
|
|||
|
|
// self.moreView.snp.updateConstraints { make in
|
|||
|
|
// make.top.equalTo(self.inputBar.snp.bottom).offset(-UIWindow.safeAreaBottom)
|
|||
|
|
// }
|
|||
|
|
// self.view.layoutIfNeeded()
|
|||
|
|
// } completion: { _ in
|
|||
|
|
// }
|
|||
|
|
// } else {
|
|||
|
|
// UIView.animate(withDuration: 0.3) {
|
|||
|
|
// self.moreView.alpha = 0
|
|||
|
|
// self.moreView.snp.updateConstraints { make in
|
|||
|
|
// make.top.equalTo(self.inputBar.snp.bottom).offset(10)
|
|||
|
|
// }
|
|||
|
|
// self.view.layoutIfNeeded()
|
|||
|
|
// } completion: { _ in
|
|||
|
|
// }
|
|||
|
|
// }
|
|||
|
|
// }
|
|||
|
|
func updateInputEntrance(offsetY: CGFloat, duration: TimeInterval, curve: Int) {
|
|||
|
|
let offsetY = offsetY > 0 ? offsetY : (UIWindow.safeAreaBottom)
|
|||
|
|
|
|||
|
|
self.bottomViewsStackV.snp.updateConstraints { make in
|
|||
|
|
make.bottom.equalTo(self.view).offset(-offsetY)
|
|||
|
|
}
|
|||
|
|
// self.bottomViewsStackV.setNeedsDisplay()
|
|||
|
|
// self.bottomViewsStackV.layoutIfNeeded()
|
|||
|
|
//
|
|||
|
|
// // 添加这行来动态调整tableView的contentInset
|
|||
|
|
// DispatchQueue.main.async { [weak self] in
|
|||
|
|
// self?.adjustTableViewContentInset()
|
|||
|
|
// }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func updateTableContentOffset(duration: TimeInterval){
|
|||
|
|
UIView.animate(withDuration: duration) {
|
|||
|
|
self.updateTableViewContentLayout()
|
|||
|
|
self.view.layoutIfNeeded()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|