jsdw_ios/QuickLocation/Section/Group/GroupChat/GroupChatVC.swift

212 lines
7.6 KiB
Swift

//
// GroupChatVC.swift
// QuickLocation
//
// Created by on 2026/6/4.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import OpenIMSDK
final class GroupChatVC: BaseViewController {
override var isNavigationBarHidden: Bool { true }
fileprivate var rootView: GroupChatView!
private let viewModel = GroupChatViewModel()
private var msgListener: MessageListenerProxy?
// MARK: - Init
init(groupId: String) {
viewModel.groupId = groupId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
rootView = GroupChatView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
requestGroupInfoByKey()
reactiveAction()
setupMessageListener()
}
// MARK: - Bindings
private func bindViewModel() {
viewModel.output.messages
.skip(1)
.map { [ChatSectionModel(model: "", items: $0)] }
.observe(on: MainScheduler.asyncInstance)
.bind(to: rootView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
viewModel.output.messages
.skip(1)
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] _ in
self?.scrollToBottom()
})
.disposed(by: disposeBag)
let emojiItems = UITableViewCell.emojiFileNames.map { $0 }
Observable.just([SectionModel(model: "", items: emojiItems)])
.bind(to: rootView.emojiCollectionView.rx.items(dataSource: emojiDataSource))
.disposed(by: disposeBag)
}
private var hasScrolledToBottom = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !hasScrolledToBottom {
hasScrolledToBottom = true
scrollToBottom()
}
}
private func scrollToBottom() {
let count = dataSource.sectionModels.first?.items.count ?? 0
guard count > 0 else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.rootView.tableView.layoutIfNeeded()
let indexPath = IndexPath(row: count - 1, section: 0)
self.rootView.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}
// MARK: - dataSource
private lazy var dataSource: RxTableViewSectionedReloadDataSource<ChatSectionModel> = {
RxTableViewSectionedReloadDataSource<ChatSectionModel> { _, tableView, indexPath, item in
switch item {
case let .send(msg):
let cell: TextSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .received(msg):
let cell: TextReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .emojiSend(msg):
let cell: EmojiSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .emojiReceived(msg):
let cell: EmojiReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .voiceSend(msg):
let cell: VoiceSendMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .voiceReceived(msg):
let cell: VoiceReceivedMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(msg)
return cell
case let .notification(text):
let cell: NotificationMsgCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(text)
return cell
}
}
}()
private lazy var emojiDataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
if let path = Bundle.main.path(forResource: name, ofType: "json") {
cell.configure(path: path)
}
return cell
}
}()
// MARK: - Message Listener
private func setupMessageListener() {
msgListener = MessageListenerProxy { [weak self] msg in
guard let self = self, msg.groupID == self.viewModel.groupId else { return }
self.viewModel.onReceiveMessage(msg)
}
OIMManager.callbacker.addAdvancedMsgListener(listener: msgListener!)
}
// MARK: - Actions
private func reactiveAction() {
rootView.backBtn.rx.tap
.subscribe(onNext: { _ in
AppRouter.shared.popOrDismiss()
})
.disposed(by: disposeBag)
rootView.emojiBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
let show = self.rootView.emojiPanelView.isHidden
self.rootView.emojiPanelView.isHidden = !show
let offset: CGFloat = show ? -220 : 0
UIView.animate(withDuration: 0.25) {
self.rootView.bottomBar.transform = CGAffineTransform(translationX: 0, y: offset)
self.rootView.tableView.transform = CGAffineTransform(translationX: 0, y: offset)
}
if show {
self.rootView.textField.resignFirstResponder()
EmojiPanelCell.preloadAnimations()
}
})
.disposed(by: disposeBag)
rootView.emojiCollectionView.rx.modelSelected(String.self)
.subscribe(onNext: { [weak self] name in
guard let self = self, let idx = UITableViewCell.emojiFileNames.firstIndex(of: name) else { return }
self.viewModel.input.sendMessage.onNext("js_emoji:\(idx)")
})
.disposed(by: disposeBag)
let sendText = Observable.merge(
rootView.sendBtn.rx.tap.map { [weak self] _ in self?.rootView.textField.text ?? "" },
rootView.textField.rx.controlEvent(.editingDidEndOnExit)
.map { [weak self] _ in self?.rootView.textField.text ?? "" }
)
.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
.do(onNext: { [weak self] _ in self?.rootView.textField.text = "" })
sendText
.bind(to: viewModel.input.sendMessage)
.disposed(by: disposeBag)
}
// MARK: - API
private func requestGroupInfoByKey() {
GroupService.groupInfoByKey(viewModel.groupId).subscribe { response in
guard let model = response.model else { return }
self.viewModel.memberList = response.list
self.rootView.groupNameLabel.text = model.name
self.rootView.groupAvatarView.image = model.groupIcon
self.viewModel.loadMessages()
}.disposed(by: disposeBag)
}
}
// MARK: - MessageListenerProxy
private class MessageListenerProxy: NSObject, OIMAdvancedMsgListener {
private let handler: (OIMMessageInfo) -> Void
init(handler: @escaping (OIMMessageInfo) -> Void) {
self.handler = handler
}
func onRecvNewMessage(_ msg: OIMMessageInfo) {
handler(msg)
}
}