| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- //
- // LNImageFeedDetailViewController.swift
- // Gami
- //
- // Created by OneeChan on 2026/3/3.
- //
- import Foundation
- import UIKit
- import SnapKit
- import MJRefresh
- extension UIView {
- func pushToImageFeedDetail(id: String) {
- let vc = LNImageFeedDetailViewController()
- vc.loadDetail(id: id)
- navigationController?.pushViewController(vc, animated: true)
- }
- }
- class LNImageFeedDetailViewController: LNViewController {
- private let avatar = UIImageView()
- private let nameLabel = UILabel()
-
- private let tableView = UITableView(frame: .zero, style: .grouped)
-
- private let likeView = LNFeedLikeView()
- private let commentView = LNFeedCommentView()
-
- private var curDetail: LNFeedDetailVO?
- private var nextTag: String?
- private var comments: [LNFeedCommentVO] = []
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setupViews()
- }
-
- func loadDetail(id: String) {
- LNFeedManager.shared.getFeedDetail(id: id) { [weak self] detail in
- guard let self else { return }
- guard let detail else {
- navigationController?.popViewController(animated: true)
- return
- }
-
- curDetail = detail
- tableView.reloadData()
-
- avatar.sd_setImage(with: URL(string: detail.avatar))
- nameLabel.text = detail.nickname
- likeView.update(id: id, liked: detail.liked, count: detail.likeCount)
- commentView.update(id: id, count: detail.commentCount)
- }
- nextTag = nil
- loadComment(id: id)
- }
- }
- extension LNImageFeedDetailViewController {
- private func loadComment(id: String) {
- LNFeedManager.shared.getFeedCommentList(id: id, next: nextTag) { [weak self] res in
- guard let self else { return }
- if let res {
- if nextTag?.isEmpty != false {
- comments.removeAll()
- tableView.mj_header?.endRefreshing()
- }
- comments.append(contentsOf: res.list)
- self.nextTag = nextTag
- tableView.reloadData()
- }
-
- if nextTag?.isEmpty != false {
- tableView.mj_footer?.endRefreshingWithNoMoreData()
- } else {
- tableView.mj_footer?.endRefreshing()
- }
- }
- }
-
- private func toComment(checkScroll: Bool = true) {
- guard let curDetail else { return }
- if checkScroll, !scrollToComment() {
- return
- }
-
- let panel = LNCommonInputPanel()
- panel.maxInput = LNFeedManager.feedCommentMaxInput
- panel.handler = { [weak self] comment in
- guard let self else { return }
- LNFeedManager.shared.sendFeedComment(id: curDetail.id, content: comment) { [weak self] success in
- guard let self else { return }
- guard success else { return }
-
- let item = LNFeedCommentVO()
- item.avatar = myUserInfo.avatar
- item.nickname = myUserInfo.nickname
- item.textContent = comment
- item.createdAt = Int(curTime * 1_000)
- comments.insert(item, at: 0)
- tableView.reloadSections(.init(integer: 1), with: .automatic)
-
- curDetail.commentCount += 1
- LNFeedManager.shared.notifyFeedCommentChanged(id: curDetail.id, count: curDetail.commentCount)
- }
- }
- panel.popup()
- }
- }
- extension LNImageFeedDetailViewController: UITableViewDataSource, UITableViewDelegate {
- func numberOfSections(in tableView: UITableView) -> Int {
- 2
- }
-
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- if section == 0 {
- 1
- } else {
- comments.count
- }
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- if indexPath.section == 0 {
- let cell = tableView.dequeueReusableCell(withIdentifier: LNImageFeedHeaderCell.className, for: indexPath) as! LNImageFeedHeaderCell
- cell.update(curDetail)
- return cell
- } else {
- let cell = tableView.dequeueReusableCell(withIdentifier: LNFeedCommentCell.className, for: indexPath) as! LNFeedCommentCell
- cell.update(comments[indexPath.row])
- return cell
- }
- }
-
- func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
- if section == 0 {
- 0
- } else {
- 36
- }
- }
-
- func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- if section == 0 {
- return nil
- }
- let container = UIView()
-
- let titleLabel = UILabel()
- titleLabel.font = .body_m
- titleLabel.textColor = .text_5
- titleLabel.text = .init(key: "A00304", comments.count)
- container.addSubview(titleLabel)
- titleLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(16)
- }
-
- return container
- }
-
- func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
- 0
- }
-
- func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
- nil
- }
- }
- extension LNImageFeedDetailViewController {
- private func scrollToComment() -> Bool {
- if curDetail?.commentCount == 0 {
- return true
- }
- if tableView.contentOffset.y
- + tableView.bounds.height
- + tableView.contentInset.top
- > tableView.contentSize.height - 50 {
- return true
- }
-
- let rect = tableView.rectForHeader(inSection: 1)
- let convertedRect = tableView.convert(rect, to: tableView.superview)
-
- if convertedRect.minY - 50 <= tableView.bounds.minY + tableView.contentInset.top {
- return true
- }
-
- let indexPath = IndexPath(row: 0, section: 1)
- tableView.scrollToRow(at: indexPath, at: .top, animated: true)
- return false
- }
-
- private func setupViews() {
- setupNavBar()
-
- let bottomMenu = buildMenuView()
- view.addSubview(bottomMenu)
- bottomMenu.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
- }
-
- let listView = buildListView()
- view.addSubview(listView)
- listView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- make.bottom.equalTo(bottomMenu.snp.top)
- }
- }
-
- private func setupNavBar() {
- let menu = buildButtonMenu()
- setRightButton(menu)
-
- let container = UIView()
- avatar.layer.cornerRadius = 16
- avatar.clipsToBounds = true
- container.addSubview(avatar)
- avatar.snp.makeConstraints { make in
- make.leading.equalToSuperview()
- make.centerY.equalToSuperview()
- make.width.height.equalTo(32)
- }
-
- nameLabel.font = .heading_h3
- nameLabel.textColor = .text_5
- nameLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
- nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
- container.addSubview(nameLabel)
- nameLabel.snp.makeConstraints { make in
- make.leading.equalTo(avatar.snp.trailing).offset(12)
- make.centerY.equalToSuperview()
- make.trailing.lessThanOrEqualToSuperview().offset(-37)
- }
-
- setTitleView(container)
- container.snp.makeConstraints { make in
- make.width.equalTo(view.bounds.width).priority(.medium)
- make.height.equalTo(44)
- }
- }
-
- private func buildButtonMenu() -> UIView {
- let moreButton = UIButton()
- moreButton.setImage(.icMoreFull, for: .normal)
- moreButton.snp.makeConstraints { make in
- make.width.height.equalTo(32)
- }
- moreButton.addAction(UIAction(handler: { [weak self] _ in
- guard let self else { return }
- guard let curDetail else { return }
- LNBottomSheetMenu.showFeedMenu(detail: curDetail, view: view)
- }), for: .touchUpInside)
-
- return moreButton
- }
-
- private func buildMenuView() -> UIView {
- let container = UIView()
- container.snp.makeConstraints { make in
- make.height.equalTo(58)
- }
-
- let comment = buildComment()
- container.addSubview(comment)
- comment.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalToSuperview().offset(-16)
- }
-
- let like = buildLike()
- container.addSubview(like)
- like.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.trailing.equalTo(comment.snp.leading).offset(-20)
- }
-
- let input = buildInput()
- container.addSubview(input)
- input.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalToSuperview().offset(16)
- make.width.equalTo(190)
- }
-
- return container
- }
-
- private func buildComment() -> UIView {
- commentView.uiColor = .text_4
- commentView.onTap { [weak self] in
- guard let self else { return }
- toComment()
- }
-
- return commentView
- }
-
- private func buildLike() -> UIView {
- likeView.uiColor = .text_4
-
- return likeView
- }
-
- private func buildInput() -> UIView {
- let container = UIView()
- container.backgroundColor = .fill_2
- container.layer.cornerRadius = 19
- container.onTap { [weak self] in
- guard let self else { return }
- toComment(checkScroll: false)
- }
- container.snp.makeConstraints { make in
- make.height.equalTo(38)
- }
-
- let editIc = UIImageView()
- editIc.image = .icImChatMenuRemark.withTintColor(.text_2)
- container.addSubview(editIc)
- editIc.snp.makeConstraints { make in
- make.leading.equalToSuperview().offset(10)
- make.centerY.equalToSuperview()
- make.width.height.equalTo(24)
- }
-
- let tipsLabel = UILabel()
- tipsLabel.font = .body_m
- tipsLabel.textColor = .text_2
- tipsLabel.text = .init(key: "A00300")
- container.addSubview(tipsLabel)
- tipsLabel.snp.makeConstraints { make in
- make.centerY.equalToSuperview()
- make.leading.equalTo(editIc.snp.trailing).offset(4)
- make.trailing.equalToSuperview().offset(-10)
- }
-
- return container
- }
-
- private func buildListView() -> UIView {
- tableView.register(LNFeedCommentCell.self, forCellReuseIdentifier: LNFeedCommentCell.className)
- tableView.register(LNImageFeedHeaderCell.self, forCellReuseIdentifier: LNImageFeedHeaderCell.className)
- tableView.showsVerticalScrollIndicator = false
- tableView.showsHorizontalScrollIndicator = false
- tableView.backgroundColor = .clear
- tableView.separatorStyle = .none
- tableView.dataSource = self
- tableView.delegate = self
- tableView.allowsSelection = false
-
- let header = MJRefreshNormalHeader { [weak self] in
- guard let self else { return }
- guard let curDetail else { return }
- self.nextTag = nil
- self.loadDetail(id: curDetail.id)
- }
- header.lastUpdatedTimeLabel?.isHidden = true
- header.stateLabel?.isHidden = true
- tableView.mj_header = header
-
- let footer = MJRefreshAutoNormalFooter { [weak self] in
- guard let self else { return }
- guard let curDetail else { return }
- self.loadComment(id: curDetail.id)
- }
- footer.setTitle("", for: .noMoreData)
- footer.setTitle(.init(key: "A00046"), for: .idle)
- tableView.mj_footer = footer
-
- return tableView
- }
- }
- private class LNImageFeedHeaderCell: UITableViewCell {
- private let stackView = UIStackView()
- private let pageControl = UIPageControl()
- private let contentLabel = UILabel()
- private let timeLabel = UILabel()
- private var curImageUrls: [String] = []
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- setupViews()
- }
-
- func update(_ detail: LNFeedDetailVO?) {
- contentLabel.text = detail?.textContent
- contentLabel.superview?.isHidden = detail?.textContent.isEmpty != false
- timeLabel.text = TimeInterval((detail?.createdAt ?? 0) / 1_000).tencentIMTimeDesc
-
- let urls = detail?.medias.compactMap { $0.type == .image ? $0.url : nil} ?? []
- if urls == curImageUrls {
- return
- }
- curImageUrls = urls
-
- if urls.isEmpty {
- stackView.superview?.isHidden = true
- pageControl.superview?.isHidden = true
- return
- }
-
- stackView.arrangedSubviews.forEach {
- stackView.removeArrangedSubview($0)
- $0.removeFromSuperview()
- }
- stackView.superview?.isHidden = false
-
- let size = urls[0].extractSize
- let scale = (size.height / size.width).bounded(min: 169.0/375.0, max: 4.0/3.0)
-
- for (index, url) in urls.enumerated() {
- let imageView = buildImageView()
- imageView.sd_setImage(with: URL(string: url))
- imageView.onTap { [weak self] in
- guard let self else { return }
- presentImagePreview(urls, index)
- }
- stackView.addArrangedSubview(imageView)
- if index == 0 {
- imageView.snp.makeConstraints { make in
- make.width.equalTo(stackView.superview!)
- make.height.equalTo(imageView.snp.width).multipliedBy(scale)
- }
- }
- }
-
- pageControl.numberOfPages = urls.count
- pageControl.superview?.isHidden = false
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension LNImageFeedHeaderCell: UIScrollViewDelegate {
- func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
- checkPageIndex(scrollView)
- }
-
- func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
- checkPageIndex(scrollView)
- }
-
- func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
- checkPageIndex(scrollView)
- }
- }
- extension LNImageFeedHeaderCell {
- private func checkPageIndex(_ scrollView: UIScrollView) {
- let page = scrollView.contentOffset.x / scrollView.bounds.width
- pageControl.currentPage = Int(page)
- }
-
- private func setupViews() {
- let stackView = UIStackView()
- stackView.axis = .vertical
- stackView.spacing = 8
- contentView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.top.equalToSuperview()
- }
-
- stackView.addArrangedSubview(buildAlbum())
- stackView.addArrangedSubview(buildPage())
- stackView.addArrangedSubview(buildContent())
- stackView.addArrangedSubview(buildTime())
-
- let line = UIView()
- line.backgroundColor = .fill_2
- contentView.addSubview(line)
- line.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.top.equalTo(stackView.snp.bottom).offset(16)
- make.bottom.equalToSuperview().offset(-6).priority(.medium)
- make.height.equalTo(1)
- }
- }
-
- private func buildAlbum() -> UIView {
- let scrollView = UIScrollView()
- scrollView.isPagingEnabled = true
- scrollView.showsVerticalScrollIndicator = false
- scrollView.showsHorizontalScrollIndicator = false
- scrollView.isHidden = true
- scrollView.delegate = self
- scrollView.backgroundColor = .primary_1
-
- stackView.distribution = .fillEqually
- scrollView.addSubview(stackView)
- stackView.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview()
- make.verticalEdges.equalToSuperview()
- make.height.equalToSuperview()
- }
-
- return scrollView
- }
-
- private func buildPage() -> UIView {
- let container = UIView()
- container.isHidden = true
-
- pageControl.pageIndicatorTintColor = .text_2
- pageControl.currentPageIndicatorTintColor = .text_4
- pageControl.preferredIndicatorImage = UIImage.image(for: .text_2, size: 4, cornerRadius: 2)
- pageControl.hidesForSinglePage = true
- container.addSubview(pageControl)
- pageControl.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.verticalEdges.equalToSuperview().inset(2)
- make.height.equalTo(4)
- }
-
- return container
- }
-
- private func buildContent() -> UIView {
- let container = UIView()
-
- contentLabel.font = .body_m
- contentLabel.textColor = .text_5
- contentLabel.text = " "
- contentLabel.numberOfLines = 0
- container.addSubview(contentLabel)
- contentLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.verticalEdges.equalToSuperview()
- }
-
- return container
- }
-
- private func buildTime() -> UIView {
- let container = UIView()
-
- timeLabel.font = .body_xs
- timeLabel.textColor = .text_4
- timeLabel.text = " "
- container.addSubview(timeLabel)
- timeLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalToSuperview().inset(16)
- make.verticalEdges.equalToSuperview()
- }
-
- return container
- }
-
- private func buildImageView() -> UIImageView {
- let imageView = UIImageView()
- imageView.contentMode = .scaleAspectFit
-
- return imageView
- }
- }
|