Files
mobile-ios/Rosetta/Features/Chats/ChatList/RequestChatsView.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)
}
}
}