165 lines
6.1 KiB
Swift
165 lines
6.1 KiB
Swift
import SwiftUI
|
|
import UIKit
|
|
|
|
// MARK: - RequestChatsController (UIKit)
|
|
|
|
/// Pure UIKit UICollectionView controller for request chats list.
|
|
/// Single flat section with ChatListCell — same rendering as main chat list.
|
|
final class RequestChatsController: UIViewController {
|
|
|
|
var onSelectDialog: ((Dialog) -> Void)?
|
|
var onDeleteDialog: ((Dialog) -> Void)?
|
|
|
|
private var dialogs: [Dialog] = []
|
|
private var isSyncing: Bool = false
|
|
private var dialogMap: [String: Dialog] = [:]
|
|
|
|
private var collectionView: UICollectionView!
|
|
private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
|
|
private var cellRegistration: UICollectionView.CellRegistration<ChatListCell, Dialog>!
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = UIColor(RosettaColors.Adaptive.background)
|
|
setupCollectionView()
|
|
setupCellRegistration()
|
|
setupDataSource()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
applyBottomInsets()
|
|
}
|
|
|
|
override func viewSafeAreaInsetsDidChange() {
|
|
super.viewSafeAreaInsetsDidChange()
|
|
applyBottomInsets()
|
|
}
|
|
|
|
// MARK: - Collection View
|
|
|
|
private func setupCollectionView() {
|
|
var listConfig = UICollectionLayoutListConfiguration(appearance: .plain)
|
|
listConfig.showsSeparators = false
|
|
listConfig.backgroundColor = .clear
|
|
listConfig.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
|
|
self?.trailingSwipeActions(for: indexPath)
|
|
}
|
|
|
|
let layout = UICollectionViewCompositionalLayout.list(using: listConfig)
|
|
|
|
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
collectionView.translatesAutoresizingMaskIntoConstraints = false
|
|
collectionView.backgroundColor = UIColor(RosettaColors.Adaptive.background)
|
|
collectionView.delegate = self
|
|
collectionView.showsHorizontalScrollIndicator = false
|
|
collectionView.showsVerticalScrollIndicator = false
|
|
collectionView.alwaysBounceHorizontal = false
|
|
collectionView.alwaysBounceVertical = true
|
|
collectionView.contentInsetAdjustmentBehavior = .never
|
|
applyBottomInsets()
|
|
view.addSubview(collectionView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
])
|
|
}
|
|
|
|
private func applyBottomInsets() {
|
|
guard collectionView != nil else { return }
|
|
let inset = view.safeAreaInsets.bottom
|
|
collectionView.contentInset.bottom = inset
|
|
collectionView.verticalScrollIndicatorInsets.bottom = inset
|
|
}
|
|
|
|
private func setupCellRegistration() {
|
|
cellRegistration = UICollectionView.CellRegistration<ChatListCell, Dialog> {
|
|
[weak self] cell, indexPath, dialog in
|
|
guard let self else { return }
|
|
cell.configure(with: dialog, isSyncing: self.isSyncing)
|
|
cell.setSeparatorHidden(false)
|
|
}
|
|
}
|
|
|
|
private func setupDataSource() {
|
|
dataSource = UICollectionViewDiffableDataSource<Int, String>(
|
|
collectionView: collectionView
|
|
) { [weak self] collectionView, indexPath, itemId in
|
|
guard let self, let dialog = self.dialogMap[itemId] else {
|
|
return UICollectionViewCell()
|
|
}
|
|
return collectionView.dequeueConfiguredReusableCell(
|
|
using: self.cellRegistration,
|
|
for: indexPath,
|
|
item: dialog
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Update Data
|
|
|
|
func updateDialogs(_ newDialogs: [Dialog], isSyncing: Bool) {
|
|
self.isSyncing = isSyncing
|
|
|
|
let oldIds = dialogs.map(\.id)
|
|
let newIds = newDialogs.map(\.id)
|
|
let structureChanged = oldIds != newIds
|
|
|
|
self.dialogs = newDialogs
|
|
dialogMap.removeAll(keepingCapacity: true)
|
|
for d in newDialogs { dialogMap[d.id] = d }
|
|
|
|
guard dataSource != nil else { return }
|
|
|
|
if structureChanged {
|
|
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
|
|
snapshot.appendSections([0])
|
|
snapshot.appendItems(newIds, toSection: 0)
|
|
dataSource.apply(snapshot, animatingDifferences: true)
|
|
}
|
|
|
|
// Reconfigure visible cells
|
|
for cell in collectionView.visibleCells {
|
|
guard let indexPath = collectionView.indexPath(for: cell),
|
|
let itemId = dataSource.itemIdentifier(for: indexPath),
|
|
let chatCell = cell as? ChatListCell,
|
|
let dialog = dialogMap[itemId] else { continue }
|
|
chatCell.configure(with: dialog, isSyncing: isSyncing)
|
|
}
|
|
}
|
|
|
|
// MARK: - Swipe Actions
|
|
|
|
private func trailingSwipeActions(for indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
guard let itemId = dataSource.itemIdentifier(for: indexPath),
|
|
let dialog = dialogMap[itemId] else { return nil }
|
|
|
|
let delete = UIContextualAction(style: .destructive, title: nil) { [weak self] _, _, completion in
|
|
DispatchQueue.main.async { self?.onDeleteDialog?(dialog) }
|
|
completion(true)
|
|
}
|
|
delete.image = UIImage(systemName: "trash.fill")
|
|
delete.backgroundColor = UIColor(red: 1, green: 0.23, blue: 0.19, alpha: 1)
|
|
|
|
return UISwipeActionsConfiguration(actions: [delete])
|
|
}
|
|
}
|
|
|
|
// MARK: - UICollectionViewDelegate
|
|
|
|
extension RequestChatsController: UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
collectionView.deselectItem(at: indexPath, animated: true)
|
|
guard let itemId = dataSource.itemIdentifier(for: indexPath),
|
|
let dialog = dialogMap[itemId] else { return }
|
|
DispatchQueue.main.async { [weak self] in
|
|
self?.onSelectDialog?(dialog)
|
|
}
|
|
}
|
|
}
|