import Foundation
import SwiftUI
import ThreemaMacros
final class SettingsCollectionViewDataSource: UICollectionViewDiffableDataSource<
SettingsCollectionViewDataSource.Section,
SettingsCollectionViewDataSource.Row
> {
// MARK: - Properties
private let businessInjector = BusinessInjector.ui
private weak var collectionView: UICollectionView?
private weak var coordinator: SettingsCoordinator?
private static let defaultListCellIdentifier = "Default"
private static let hostingListCellIdentifier = "Hosting"
private let cellProvider: SettingsCollectionViewDataSource.CellProvider = { collectionView, indexPath, item in
switch item {
case .betaFeedback, .developer, .privacy, .appearance, .notifications, .chat, .media,
.storage, .passcode,
.calls, .desktop, .web, .rate, .invite, .channel, .support, .policy, .tos, .license, .advanced,
.backups:
let cell: SettingsCollectionViewCell = collectionView.dequeueCell(for: indexPath)
cell.rowType = item
return cell
case .workAd:
guard let cell: UICollectionViewListCell = collectionView.dequeueReusableCell(
withReuseIdentifier: defaultListCellIdentifier,
for: indexPath
) as? UICollectionViewListCell else {
return nil
}
var content = cell.defaultContentConfiguration()
content.text = #localize("settings_threema_work")
content.secondaryText = #localize("settings_threema_work_subtitle")
let image = UIImage(resource: .threemaWorkSettings)
content.imageProperties.cornerRadius = 8
content.imageProperties.maximumSize = CGSize(width: 45, height: 45)
content.image = image
cell.contentConfiguration = content
cell.accessories = [.disclosureIndicator()]
return cell
case .workReferral:
guard let cell: UICollectionViewListCell = collectionView.dequeueReusableCell(
withReuseIdentifier: hostingListCellIdentifier,
for: indexPath
) as? UICollectionViewListCell else {
return nil
}
let hostingConfiguration = UIHostingConfiguration {
WorkReferralCellView()
}
.background {
UIColor.primaryColorWork.resolvedColor(with: UITraitCollection(userInterfaceStyle: .light)).color
}
cell.contentConfiguration = hostingConfiguration
cell.accessories = []
return cell
case .network, .version, .workLicense, .remoteSecretState:
guard let cell: UICollectionViewListCell = collectionView.dequeueReusableCell(
withReuseIdentifier: defaultListCellIdentifier,
for: indexPath
) as? UICollectionViewListCell else {
return nil
}
var content = UIListContentConfiguration.valueCell()
content.text = item.title
if case .network = item {
let sc = BusinessInjector.ui.serverConnector
let statusText = "status_\(sc.name(for: sc.connectionState))"
.localized
.appending(sc.isIPv6Connection ? " (IPv6)" : "")
.appending(sc.isProxyConnection ? " (Proxy)" : "")
content.secondaryText = statusText
}
else if case .version = item {
let versionText = ThreemaUtility.appAndBuildVersionPretty
content.secondaryText = versionText
}
else if case .workLicense = item {
let licenseText = BusinessInjector.ui.licenseStore.licenseUsername ?? ""
content.secondaryText = licenseText
}
else if case .remoteSecretState = item {
let stateText = #localize("settings_list_remote_secret_active")
content.secondaryText = stateText
}
cell.contentConfiguration = content
cell.accessories = []
return cell
}
}
// MARK: - Lifecycle
init(collectionView: UICollectionView, coordinator: SettingsCoordinator?) {
self.collectionView = collectionView
self.coordinator = coordinator
super.init(collectionView: collectionView, cellProvider: cellProvider)
registerCells()
businessInjector.serverConnector.registerConnectionStateDelegate(delegate: self)
}
deinit {
businessInjector.serverConnector.unregisterConnectionStateDelegate(delegate: self)
}
private func registerCells() {
guard let collectionView else {
assertionFailure("Collection view must not be nil.")
return
}
collectionView.registerCell(SettingsCollectionViewCell.self)
collectionView.register(
UICollectionViewListCell.self,
forCellWithReuseIdentifier: Self.defaultListCellIdentifier
)
collectionView.register(
UICollectionViewListCell.self,
forCellWithReuseIdentifier: Self.hostingListCellIdentifier
)
}
func configureData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Row>()
// Dev
if !Section.dev.rows.isEmpty {
snapshot.appendSections([.dev])
snapshot.appendItems(Section.dev.rows)
}
// General
snapshot.appendSections([.general])
snapshot.appendItems(Section.general.rows)
// Desktop
if !Section.desktop.rows.isEmpty {
snapshot.appendSections([.desktop])
snapshot.appendItems(Section.desktop.rows)
}
// Status
snapshot.appendSections([.status])
snapshot.appendItems(Section.status.rows)
// Work Add
if !Section.workAdvertising.rows.isEmpty {
snapshot.appendSections([.workAdvertising])
snapshot.appendItems(Section.workAdvertising.rows)
}
// Work Referral
if !Section.workReferral.rows.isEmpty {
snapshot.appendSections([.workReferral])
snapshot.appendItems(Section.workReferral.rows)
}
// Social
if !Section.social.rows.isEmpty {
snapshot.appendSections([.social])
snapshot.appendItems(Section.social.rows)
}
// Support
snapshot.appendSections([.support])
snapshot.appendItems(Section.support.rows)
apply(snapshot)
}
func indexPathForItem(_ item: Row) -> IndexPath? {
indexPath(for: item)
}
func canSelectItem(at indexPath: IndexPath) -> Bool {
guard let identifier = itemIdentifier(for: indexPath) else {
return false
}
if case .network = identifier {
return false
}
else if case .workLicense = identifier {
return false
}
else {
return true
}
}
func didSelectItem(at indexPath: IndexPath) {
guard let coordinator, let identifier = itemIdentifier(for: indexPath) else {
return
}
switch identifier {
case .betaFeedback:
coordinator.show(.betaFeedback)
case .developer:
coordinator.show(.developer)
case .privacy:
coordinator.show(.privacy)
case .appearance:
coordinator.show(.appearance)
case .notifications:
coordinator.show(.notifications)
case .chat:
coordinator.show(.chat)
case .media:
coordinator.show(.media)
case .backups:
coordinator.show(.backups(.center))
case .storage:
coordinator.show(.storage)
case .passcode:
coordinator.show(.passcode)
case .calls:
coordinator.show(.calls)
case .desktop:
coordinator.show(.desktop)
case .web:
coordinator.show(.web)
case .network:
break
case .version:
versionSelected(at: indexPath)
case .workLicense:
break
case .remoteSecretState:
break
case .rate:
rateSelected(at: indexPath)
case .invite:
coordinator.showInviteActivity(from: collectionView?.cellForItem(at: indexPath))
case .channel:
coordinator.show(.channel)
case .workAd:
workAdSelected(at: indexPath)
case .workReferral:
coordinator.show(.workReferral)
case .support:
coordinator.show(.support)
case .policy:
coordinator.show(.policy)
case .tos:
coordinator.show(.tos)
case .license:
coordinator.show(.license)
case .advanced:
coordinator.show(.advanced)
}
}
private func versionSelected(at indexPath: IndexPath) {
UIPasteboard.general.string = ThreemaUtility.appAndBuildVersionPretty
NotificationPresenterWrapper.shared.present(type: .copySuccess)
collectionView?.deselectItem(at: indexPath, animated: true)
}
private func rateSelected(at indexPath: IndexPath) {
if let link = TargetManager.rateLink {
UIApplication.shared.open(link, options: [:], completionHandler: nil)
}
collectionView?.deselectItem(at: indexPath, animated: true)
}
private func workAdSelected(at indexPath: IndexPath) {
if let appURL = URL(string: "threemawork://app"), UIApplication.shared.canOpenURL(appURL) {
UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
collectionView?.deselectItem(at: indexPath, animated: true)
}
else {
coordinator?.show(.workInfo)
}
}
}
// MARK: - ConnectionStateDelegate
extension SettingsCollectionViewDataSource: ConnectionStateDelegate {
func changed(connectionState state: ConnectionState) {
Task { @MainActor in
var snapshot = snapshot()
snapshot.reconfigureItems([.network])
apply(snapshot)
}
}
}
¤ Dauer der Verarbeitung: 0.1 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland