jsdw_ios/QuickLocation/Section/Group/Mailbox/MailboxViewController.swift

373 lines
12 KiB
Swift

//
// MailboxViewController.swift
// QuickLocation
//
import UIKit
import RxSwift
import MJRefresh
final class MailboxViewController: BaseViewController {
override var isNavigationBarHidden: Bool { true }
private enum LoadKind {
case initial
case refresh
case more
case category
}
private var rootView: MailboxView!
private var messages: [MailboxMessage] = []
private var selectedCategory: MailboxCategory = .interaction
private var currentPage = 0
private var total = 0
private var reachedEnd = false
private var isLoading = false
private var pendingCategory: MailboxCategory?
override func loadView() {
rootView = MailboxView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
bindActions()
rootView.selectCategory(selectedCategory)
updateContentState(showLoading: true)
loadPage(
1,
kind: .initial,
replacing: true,
category: selectedCategory,
baselineCount: 0
)
}
private var visibleMessages: [MailboxMessage] {
messages.filter { $0.category == selectedCategory }
}
private func setupTableView() {
rootView.tableView.dataSource = self
rootView.tableView.delegate = self
let header = MJRefreshNormalHeader { [weak self] in
self?.refreshMessages()
}
header.addFeedbackGenerator()
header.stateLabel?.isHidden = true
rootView.tableView.mj_header = header
let footer = MJRefreshAutoNormalFooter { [weak self] in
self?.loadMoreMessages()
}
footer.stateLabel?.isHidden = true
footer.isHidden = true
rootView.tableView.mj_footer = footer
}
private func bindActions() {
rootView.backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
rootView.clearButton.addTarget(self, action: #selector(clearAction), for: .touchUpInside)
rootView.onSelectCategory = { [weak self] category in
self?.selectCategory(category)
}
}
private func selectCategory(_ category: MailboxCategory) {
guard selectedCategory != category else { return }
selectedCategory = category
rootView.selectCategory(category)
rootView.tableView.reloadData()
rootView.tableView.setContentOffset(
CGPoint(x: 0, y: -rootView.tableView.adjustedContentInset.top),
animated: false
)
updateContentState(showLoading: isLoading)
ensureMessagesLoaded(for: category)
}
private func refreshMessages() {
guard !isLoading else {
rootView.tableView.mj_header?.endRefreshing()
return
}
loadPage(
1,
kind: .refresh,
replacing: true,
category: selectedCategory,
baselineCount: 0
)
}
private func loadMoreMessages() {
guard !isLoading, !reachedEnd else {
finishRefreshControls()
return
}
let category = selectedCategory
loadPage(
currentPage + 1,
kind: .more,
replacing: false,
category: category,
baselineCount: messages(for: category).count
)
}
private func ensureMessagesLoaded(for category: MailboxCategory) {
guard messages(for: category).isEmpty, !reachedEnd else {
updateContentState(showLoading: false)
return
}
guard !isLoading else {
pendingCategory = category
return
}
loadPage(
currentPage + 1,
kind: .category,
replacing: false,
category: category,
baselineCount: 0
)
}
private func loadPage(
_ page: Int,
kind: LoadKind,
replacing: Bool,
category: MailboxCategory,
baselineCount: Int
) {
guard !isLoading else { return }
isLoading = true
pendingCategory = nil
updateContentState(showLoading: visibleMessages.isEmpty)
UserService.mailboxList(page: page)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
guard let data = response.data else {
self.finishLoadingWithError(message: response.message ?? "消息加载失败")
return
}
if replacing {
self.messages = data.messages
} else {
self.mergeMessages(data.messages)
}
self.currentPage = max(data.page, page)
self.total = max(data.total, self.messages.count)
self.reachedEnd = self.calculateReachedEnd(data: data)
self.rootView.tableView.reloadData()
self.updateUnreadCounts()
let categoryCount = self.messages(for: category).count
if self.selectedCategory == category,
categoryCount <= baselineCount,
!self.reachedEnd {
self.isLoading = false
self.loadPage(
self.currentPage + 1,
kind: kind,
replacing: false,
category: category,
baselineCount: baselineCount
)
return
}
self.isLoading = false
self.finishRefreshControls()
self.updateContentState(showLoading: false)
self.loadPendingCategoryIfNeeded()
}, onError: { [weak self] error in
self?.finishLoadingWithError(message: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func mergeMessages(_ newMessages: [MailboxMessage]) {
var indices = Dictionary(uniqueKeysWithValues: messages.enumerated().map { ($0.element.id, $0.offset) })
for message in newMessages {
if let index = indices[message.id] {
messages[index] = message
} else {
indices[message.id] = messages.count
messages.append(message)
}
}
}
private func calculateReachedEnd(data: MailboxListData) -> Bool {
if data.total == 0 || data.messages.isEmpty {
return true
}
if messages.count >= data.total {
return true
}
let pageSize = max(data.size, 1)
return max(data.page, currentPage) * pageSize >= data.total
}
private func loadPendingCategoryIfNeeded() {
guard let category = pendingCategory ?? (visibleMessages.isEmpty ? selectedCategory : nil) else { return }
pendingCategory = nil
ensureMessagesLoaded(for: category)
}
private func finishLoadingWithError(message: String) {
isLoading = false
finishRefreshControls()
updateContentState(showLoading: false)
DLToast.showError(text: message)
}
private func finishRefreshControls() {
rootView.tableView.mj_header?.endRefreshing()
if reachedEnd {
rootView.tableView.mj_footer?.endRefreshingWithNoMoreData()
} else {
rootView.tableView.mj_footer?.endRefreshing()
}
}
private func messages(for category: MailboxCategory) -> [MailboxMessage] {
messages.filter { $0.category == category }
}
private func updateUnreadCounts() {
var counts: [MailboxCategory: Int] = [:]
MailboxCategory.displayOrder.forEach { category in
counts[category] = messages(for: category).filter { !$0.isRead }.count
}
rootView.updateUnreadCounts(counts)
}
private func updateContentState(showLoading: Bool) {
rootView.updateState(
hasMessages: !visibleMessages.isEmpty,
isLoading: showLoading,
reachedEnd: reachedEnd
)
}
private func openMessage(_ message: MailboxMessage) {
DLToast.showLoading()
UserService.mailboxDetail(id: message.id)
.subscribe(onNext: { [weak self] response in
guard let self else { return }
DLToast.dismiss()
guard let responseMessage = response.data else {
DLToast.showError(text: response.message ?? "消息详情加载失败")
return
}
var detail = self.mergeDetail(responseMessage, fallback: message)
detail.isRead = true
self.updateMessage(detail)
let categoryMessages = self.messages(for: detail.category)
let detailController = MailboxDetailViewController(
messages: categoryMessages,
initialMessage: detail
)
detailController.onMessageRead = { [weak self] updatedMessage in
self?.updateMessage(updatedMessage)
}
self.present(detailController, animated: true)
}, onError: { error in
DLToast.dismiss()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
private func updateMessage(_ updatedMessage: MailboxMessage) {
guard let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) else { return }
messages[index] = updatedMessage
rootView.tableView.reloadData()
updateUnreadCounts()
}
private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage {
var merged = detail
if merged.id == 0 { merged.id = fallback.id }
if merged.title.isEmpty { merged.title = fallback.title }
if merged.content.isEmpty { merged.content = fallback.content }
if merged.senderId.isEmpty { merged.senderId = fallback.senderId }
if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon }
if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime }
if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type }
return merged
}
@objc private func backAction() {
navigationController?.popViewController(animated: true)
}
@objc private func clearAction() {
guard !messages.isEmpty else {
DLToast.show(text: "暂无消息")
return
}
showConfirmPop(
title: "清空全部消息",
message: "清空后无法恢复,是否继续?",
confirmText: "清空",
confirmBlock: { [weak self] in
self?.requestClearAll()
},
cancelText: "取消"
)
}
private func requestClearAll() {
DLToast.showLoading()
UserService.clearMailbox()
.subscribe(onNext: { [weak self] response in
guard let self else { return }
DLToast.dismiss()
self.messages = []
self.currentPage = 0
self.total = 0
self.reachedEnd = true
self.pendingCategory = nil
self.rootView.tableView.reloadData()
self.updateUnreadCounts()
self.finishRefreshControls()
self.updateContentState(showLoading: false)
DLToast.showSuccess(text: "已清空")
}, onError: { error in
DLToast.dismiss()
DLToast.showError(text: error.localizedDescription)
})
.disposed(by: disposeBag)
}
}
extension MailboxViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
visibleMessages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: MailboxMessageCell.reuseIdentifier,
for: indexPath
) as? MailboxMessageCell else {
return UITableViewCell()
}
cell.configure(message: visibleMessages[indexPath.row])
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
openMessage(visibleMessages[indexPath.row])
}
}