// // 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 = { RxTableViewSectionedReloadDataSource { _, 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> = { RxCollectionViewSectionedReloadDataSource> { _, 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) } }