393 lines
18 KiB
Swift
393 lines
18 KiB
Swift
//
|
|
// MailboxDetailViewController.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import RxSwift
|
|
import Lottie
|
|
|
|
final class MailboxDetailViewController: UIViewController {
|
|
private var messages: [MailboxMessage]
|
|
private var currentIndex: Int
|
|
private var currentMessage: MailboxMessage
|
|
private let disposeBag = DisposeBag()
|
|
private let detailView = MailboxDetailView()
|
|
private var isLoading = false
|
|
|
|
var onMessageRead: ((MailboxMessage) -> Void)?
|
|
|
|
init(messages: [MailboxMessage], initialMessage: MailboxMessage) {
|
|
self.messages = messages
|
|
self.currentMessage = initialMessage
|
|
self.currentIndex = messages.firstIndex(where: { $0.id == initialMessage.id }) ?? 0
|
|
super.init(nibName: nil, bundle: nil)
|
|
modalPresentationStyle = .overFullScreen
|
|
modalTransitionStyle = .crossDissolve
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func loadView() {
|
|
view = detailView
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
detailView.closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
|
|
detailView.previousButton.addTarget(self, action: #selector(previousAction), for: .touchUpInside)
|
|
detailView.nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside)
|
|
renderCurrentMessage()
|
|
}
|
|
|
|
private func renderCurrentMessage() {
|
|
detailView.configure(
|
|
message: currentMessage,
|
|
canGoPrevious: currentIndex > 0,
|
|
canGoNext: currentIndex + 1 < messages.count
|
|
)
|
|
}
|
|
|
|
private func loadMessage(at index: Int) {
|
|
guard messages.indices.contains(index), !isLoading else { return }
|
|
let cachedMessage = messages[index]
|
|
isLoading = true
|
|
detailView.setControlsEnabled(false)
|
|
DLToast.showLoading()
|
|
|
|
UserService.mailboxDetail(id: cachedMessage.id)
|
|
.subscribe(onNext: { [weak self] response in
|
|
guard let self else { return }
|
|
DLToast.dismiss()
|
|
self.isLoading = false
|
|
guard let responseMessage = response.data else {
|
|
self.detailView.setControlsEnabled(true)
|
|
DLToast.showError(text: response.message ?? "消息详情加载失败")
|
|
return
|
|
}
|
|
var detail = self.mergeDetail(responseMessage, fallback: cachedMessage)
|
|
detail.isRead = true
|
|
self.messages[index] = detail
|
|
self.currentIndex = index
|
|
self.currentMessage = detail
|
|
self.onMessageRead?(detail)
|
|
self.renderCurrentMessage()
|
|
}, onError: { [weak self] error in
|
|
DLToast.dismiss()
|
|
self?.isLoading = false
|
|
self?.detailView.setControlsEnabled(true)
|
|
DLToast.showError(text: error.localizedDescription)
|
|
})
|
|
.disposed(by: disposeBag)
|
|
}
|
|
|
|
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 closeAction() {
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@objc private func previousAction() {
|
|
loadMessage(at: currentIndex - 1)
|
|
}
|
|
|
|
@objc private func nextAction() {
|
|
loadMessage(at: currentIndex + 1)
|
|
}
|
|
}
|
|
|
|
private final class MailboxDetailView: UIView {
|
|
let previousButton = UIButton(type: .custom)
|
|
let closeButton = UIButton(type: .custom)
|
|
let nextButton = UIButton(type: .custom)
|
|
|
|
private let cardView = UIView()
|
|
private let cardBackgroundView = UIImageView()
|
|
private let birdImageView = UIImageView(image: UIImage(named: "Mailbox/detail_bird"))
|
|
private let typeLabel = UILabel()
|
|
private let dividerView = UIView()
|
|
private let titleLabel = UILabel()
|
|
private let scrollView = UIScrollView()
|
|
private let contentContainerView = UIView()
|
|
private let contentLabel = UILabel()
|
|
private let lottieView = LottieAnimationView()
|
|
private let quickMessageLabel = UILabel()
|
|
private var animationName: String?
|
|
private var contentBottomConstraint: NSLayoutConstraint?
|
|
private var cardHeightConstraint: NSLayoutConstraint?
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(message: MailboxMessage, canGoPrevious: Bool, canGoNext: Bool) {
|
|
typeLabel.text = message.category.detailTitle
|
|
titleLabel.text = message.title
|
|
configureContent(message: message)
|
|
previousButton.isEnabled = canGoPrevious
|
|
nextButton.isEnabled = canGoNext
|
|
updateButtonAppearance()
|
|
}
|
|
|
|
func setControlsEnabled(_ enabled: Bool) {
|
|
previousButton.isUserInteractionEnabled = enabled
|
|
nextButton.isUserInteractionEnabled = enabled
|
|
closeButton.isUserInteractionEnabled = enabled
|
|
previousButton.alpha = enabled ? (previousButton.isEnabled ? 1 : 0.35) : 0.35
|
|
nextButton.alpha = enabled ? (nextButton.isEnabled ? 1 : 0.35) : 0.35
|
|
closeButton.alpha = enabled ? 1 : 0.55
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundColor = UIColor.black.withAlphaComponent(0.68)
|
|
|
|
cardView.layer.cornerRadius = 31
|
|
cardView.clipsToBounds = true
|
|
cardBackgroundView.image = UIImage(named: "Mailbox/detail_card")?.resizableImage(
|
|
withCapInsets: UIEdgeInsets(top: 70, left: 70, bottom: 70, right: 70),
|
|
resizingMode: .stretch
|
|
)
|
|
cardBackgroundView.contentMode = .scaleToFill
|
|
|
|
birdImageView.contentMode = .scaleAspectFit
|
|
|
|
typeLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
|
typeLabel.textColor = UIColor(hexStr: "#14324E")
|
|
typeLabel.textAlignment = .center
|
|
|
|
dividerView.backgroundColor = UIColor(hexStr: "#80D8FF", alpha: 0.55)
|
|
|
|
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
|
titleLabel.textColor = UIColor(hexStr: "#333333")
|
|
titleLabel.textAlignment = .center
|
|
titleLabel.numberOfLines = 0
|
|
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.alwaysBounceVertical = false
|
|
|
|
contentLabel.font = .systemFont(ofSize: 15)
|
|
contentLabel.textColor = UIColor(hexStr: "#858585")
|
|
contentLabel.textAlignment = .center
|
|
contentLabel.numberOfLines = 0
|
|
|
|
lottieView.contentMode = .scaleAspectFit
|
|
lottieView.loopMode = .loop
|
|
lottieView.backgroundBehavior = .pauseAndRestore
|
|
|
|
quickMessageLabel.font = .systemFont(ofSize: 15)
|
|
quickMessageLabel.textColor = UIColor(hexStr: "#52606D")
|
|
quickMessageLabel.textAlignment = .center
|
|
quickMessageLabel.numberOfLines = 0
|
|
quickMessageLabel.backgroundColor = UIColor(hexStr: "#DFF5FF")
|
|
quickMessageLabel.layer.cornerRadius = 9
|
|
quickMessageLabel.clipsToBounds = true
|
|
|
|
configureControlButton(previousButton, title: "上一条")
|
|
configureControlButton(nextButton, title: "下一条")
|
|
closeButton.setImage(UIImage(named: "Mailbox/detail_close"), for: .normal)
|
|
closeButton.accessibilityLabel = "关闭"
|
|
|
|
addSubview(cardView)
|
|
cardView.addSubview(cardBackgroundView)
|
|
addSubview(birdImageView)
|
|
cardView.addSubview(typeLabel)
|
|
cardView.addSubview(dividerView)
|
|
cardView.addSubview(titleLabel)
|
|
cardView.addSubview(scrollView)
|
|
scrollView.addSubview(contentContainerView)
|
|
contentContainerView.addSubview(contentLabel)
|
|
contentContainerView.addSubview(lottieView)
|
|
contentContainerView.addSubview(quickMessageLabel)
|
|
addSubview(previousButton)
|
|
addSubview(closeButton)
|
|
addSubview(nextButton)
|
|
|
|
[cardView, cardBackgroundView, birdImageView, typeLabel, dividerView, titleLabel, scrollView,
|
|
contentContainerView, contentLabel, lottieView, quickMessageLabel, previousButton, closeButton,
|
|
nextButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
|
|
|
let cardWidth = min(kScreenWidth - 48, 360)
|
|
let cardHeightConstraint = cardView.heightAnchor.constraint(equalToConstant: 245)
|
|
self.cardHeightConstraint = cardHeightConstraint
|
|
NSLayoutConstraint.activate([
|
|
cardView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
cardView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -32),
|
|
cardView.widthAnchor.constraint(equalToConstant: cardWidth),
|
|
cardHeightConstraint,
|
|
cardBackgroundView.topAnchor.constraint(equalTo: cardView.topAnchor),
|
|
cardBackgroundView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
|
|
cardBackgroundView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
|
|
cardBackgroundView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor),
|
|
birdImageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 36),
|
|
birdImageView.bottomAnchor.constraint(equalTo: cardView.topAnchor, constant: 35),
|
|
birdImageView.widthAnchor.constraint(equalToConstant: 100),
|
|
birdImageView.heightAnchor.constraint(equalToConstant: 74),
|
|
typeLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 49),
|
|
typeLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 26),
|
|
typeLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -26),
|
|
dividerView.topAnchor.constraint(equalTo: typeLabel.bottomAnchor, constant: 15),
|
|
dividerView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 23),
|
|
dividerView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -23),
|
|
dividerView.heightAnchor.constraint(equalToConstant: 0.5),
|
|
titleLabel.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 20),
|
|
titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 30),
|
|
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -30),
|
|
scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 13),
|
|
scrollView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 28),
|
|
scrollView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -28),
|
|
scrollView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -25),
|
|
contentContainerView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
|
contentContainerView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
|
contentContainerView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
|
contentContainerView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
|
contentContainerView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
|
contentLabel.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
|
|
contentLabel.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor),
|
|
contentLabel.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor),
|
|
lottieView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 10),
|
|
lottieView.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
|
|
lottieView.widthAnchor.constraint(equalToConstant: 116),
|
|
lottieView.heightAnchor.constraint(equalToConstant: 116),
|
|
quickMessageLabel.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 14),
|
|
quickMessageLabel.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
|
|
quickMessageLabel.leadingAnchor.constraint(greaterThanOrEqualTo: contentContainerView.leadingAnchor, constant: 20),
|
|
quickMessageLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentContainerView.trailingAnchor, constant: -20),
|
|
quickMessageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
|
|
previousButton.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 20),
|
|
previousButton.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 34),
|
|
previousButton.heightAnchor.constraint(equalToConstant: 40),
|
|
nextButton.topAnchor.constraint(equalTo: previousButton.topAnchor),
|
|
nextButton.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -34),
|
|
nextButton.heightAnchor.constraint(equalTo: previousButton.heightAnchor),
|
|
closeButton.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
closeButton.centerYAnchor.constraint(equalTo: previousButton.centerYAnchor),
|
|
closeButton.widthAnchor.constraint(equalToConstant: 24),
|
|
closeButton.heightAnchor.constraint(equalToConstant: 24)
|
|
])
|
|
}
|
|
|
|
private func configureControlButton(_ button: UIButton, title: String) {
|
|
button.setTitle(title, for: .normal)
|
|
button.setTitleColor(.white, for: .normal)
|
|
button.setTitleColor(UIColor.white.withAlphaComponent(0.45), for: .disabled)
|
|
button.titleLabel?.font = .systemFont(ofSize: 16)
|
|
}
|
|
|
|
private func configureContent(message: MailboxMessage) {
|
|
animationName = nil
|
|
lottieView.stop()
|
|
lottieView.animation = nil
|
|
lottieView.isHidden = true
|
|
quickMessageLabel.isHidden = true
|
|
contentLabel.text = message.content
|
|
cardHeightConstraint?.constant = message.content.count > 80 ? 300 : 245
|
|
|
|
guard message.category == .interaction,
|
|
let payload = parsePayload(message.content) else {
|
|
updateContentBottomAnchor(visibleView: contentLabel)
|
|
return
|
|
}
|
|
|
|
contentLabel.text = payload.prefix
|
|
switch payload.kind {
|
|
case let .animation(name):
|
|
cardHeightConstraint?.constant = 330
|
|
animationName = name
|
|
lottieView.isHidden = false
|
|
updateContentBottomAnchor(visibleView: lottieView)
|
|
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
|
|
guard let self, self.animationName == name else { return }
|
|
guard let animation else {
|
|
self.showFallbackContent(message.content)
|
|
return
|
|
}
|
|
self.lottieView.animation = animation
|
|
self.lottieView.currentProgress = 0
|
|
self.lottieView.play()
|
|
}
|
|
case let .quickMessage(text):
|
|
cardHeightConstraint?.constant = 265
|
|
quickMessageLabel.text = " \(text) "
|
|
quickMessageLabel.isHidden = false
|
|
updateContentBottomAnchor(visibleView: quickMessageLabel)
|
|
}
|
|
}
|
|
|
|
private func updateContentBottomAnchor(visibleView: UIView) {
|
|
contentBottomConstraint?.isActive = false
|
|
contentBottomConstraint = visibleView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor)
|
|
contentBottomConstraint?.isActive = true
|
|
}
|
|
|
|
private func showFallbackContent(_ content: String) {
|
|
animationName = nil
|
|
lottieView.stop()
|
|
lottieView.animation = nil
|
|
lottieView.isHidden = true
|
|
quickMessageLabel.isHidden = true
|
|
contentLabel.text = content
|
|
updateContentBottomAnchor(visibleView: contentLabel)
|
|
}
|
|
|
|
private func parsePayload(_ content: String) -> MailboxPayload? {
|
|
guard let opening = content.firstIndex(of: "["),
|
|
let closing = content[opening...].firstIndex(of: "]"),
|
|
let lastOpening = content.lastIndex(of: "[") else {
|
|
return nil
|
|
}
|
|
let codeStart = content.index(after: opening)
|
|
let code = String(content[codeStart..<closing])
|
|
guard code.count > 1, let prefixCode = code.first else { return nil }
|
|
let value = String(code.dropFirst())
|
|
let prefix = String(content[..<lastOpening]).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
switch prefixCode {
|
|
case "1":
|
|
guard Int(value) != nil else { return nil }
|
|
return MailboxPayload(prefix: prefix, kind: .animation(name: "normal_\(value)"))
|
|
case "2":
|
|
guard Int(value) != nil else { return nil }
|
|
return MailboxPayload(prefix: prefix, kind: .animation(name: "fun_\(value)"))
|
|
case "3":
|
|
guard let index = Int(value), QuickMessageView.messageList.indices.contains(index) else { return nil }
|
|
return MailboxPayload(prefix: prefix, kind: .quickMessage(QuickMessageView.messageList[index]))
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private func updateButtonAppearance() {
|
|
previousButton.alpha = previousButton.isEnabled ? 1 : 0.35
|
|
nextButton.alpha = nextButton.isEnabled ? 1 : 0.35
|
|
closeButton.alpha = 1
|
|
setControlsEnabled(true)
|
|
}
|
|
}
|
|
|
|
private struct MailboxPayload {
|
|
enum Kind {
|
|
case animation(name: String)
|
|
case quickMessage(String)
|
|
}
|
|
|
|
let prefix: String
|
|
let kind: Kind
|
|
}
|