Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Threema/Threema/SharedUI/   (Social Medium ©)  Datei vom 26.9.2026 mit Größe 15 kB image not shown  

Quelle  DoNotDisturbViewController.swift   Sprache: JAVA

 

import ThreemaMacros
import UIKit

/// Show and change `PushSetting` for a conversation or contact
///
/// It is only optimized to be shown modally. Please wrap it into a `ThemedNavigationController` when doing so. It will
/// dismiss itself.
///
/// Don't change the settings of the same conversation or contact at another place the same time. Otherwise they changes
/// will be overridden when the controller dismisses itself.
final class DoNotDisturbViewController: ThemedCodeModernGroupedTableViewController {

    // MARK: - Private types

    private enum Section: Hashable {
        case activeDND
        case selectPeriod
        case notifyWhenMentionedSetting
        case notificationPlaySoundSetting
    }
    
    private enum Row: Hashable {
        case activeDNDInfo
        case turnOffDNDButton
        case periodButton(duration: PushSetting.PeriodOffTime)
        case foreverButton
        case notifyWhenMentionedSetting
        case notificationPlaySoundSetting
    }
        
    // MARK: - Properties

    private lazy var businessInjector = BusinessInjector.ui

    private lazy var dataSource = TableViewDiffableSimpleHeaderAndFooterDataSource<Section, Row>(
        tableView: tableView,
        cellProvider: { [weak self] tableView, indexPath, row -> UITableViewCell? in
        
            switch row {
            case .activeDNDInfo:
                let activeDNDInfoCell: ActiveDNDInfoCell = tableView.dequeueCell(for: indexPath)
            
                // Show current state icon if there is any
                if let symbolName = self?.pushSetting.sfSymbolNameForPushSetting {
                    var image = UIImage(systemName: symbolName)
                    if image == nil {
                        image = UIImage(named: symbolName)
                    }
                    image = image?.applying(symbolWeight: .regular, symbolScale: .medium)
                    
                    assert(image != nil, "Symbol not found")
                    
                    activeDNDInfoCell.imageView?.image = image
                    activeDNDInfoCell.tintColor = activeDNDInfoCell.textLabel?.textColor
                }
            
                activeDNDInfoCell.textLabel?.text = self?.pushSetting.localizedLongDescription
            
                return activeDNDInfoCell
            
            case .turnOffDNDButton:
                let turnOffDNDButtonCell: TurnOffDNDButtonCell = tableView.dequeueCell(for: indexPath)
                turnOffDNDButtonCell.textLabel?.text = #localize("doNotDisturb_turn_off_button")
                return turnOffDNDButtonCell
        
            case let .periodButton(duration: duration):
                let periodButtonCell: PeriodButtonCell = tableView.dequeueCell(for: indexPath)
                periodButtonCell.textLabel?.text = duration.localizedString
                return periodButtonCell
            
            case .foreverButton:
                let periodButtonCell: PeriodButtonCell = tableView.dequeueCell(for: indexPath)
                periodButtonCell.textLabel?.text = #localize("doNotDisturb_on_forever")
                return periodButtonCell
            
            case .notifyWhenMentionedSetting:
                let notifyWhenMentionedSettingCell: NotifyWhenMentionedSettingCell = tableView
                    .dequeueCell(for: indexPath)
            
                notifyWhenMentionedSettingCell.textLabel?.text = #localize("doNotDisturb_mention")
            
                notifyWhenMentionedSettingCell.isOn = self?.pushSetting.mentioned ?? false
                notifyWhenMentionedSettingCell.valueDidChange = { [weak self] isOn in
                    Task { @MainActor in
                        self?.pushSetting.mentioned = isOn
                        if let pushSetting = self?.pushSetting {
                            await self?.businessInjector.pushSettingManager.save(
                                pushSetting: pushSetting,
                                sync: true
                            )
                        }
                        self?.updateContent()
                    }
                }
            
                return notifyWhenMentionedSettingCell
                
            case .notificationPlaySoundSetting:
                let notificationPlaySoundSettingCell: NotificationPlaySoundSettingCell = tableView
                    .dequeueCell(for: indexPath)

                notificationPlaySoundSettingCell.textLabel?.text = #localize("notification_sound_title")

                notificationPlaySoundSettingCell.isOn = !(self?.pushSetting.muted ?? false)
                notificationPlaySoundSettingCell.valueDidChange = { [weak self] isOn in
                    Task { @MainActor in
                        self?.pushSetting.muted = !isOn
                        if let pushSetting = self?.pushSetting {
                            await self?.businessInjector.pushSettingManager.save(
                                pushSetting: pushSetting,
                                sync: true
                            )
                        }
                    }
                }

                return notificationPlaySoundSettingCell
            }
        },
        headerProvider: { [weak self] _, section -> String? in
            guard let strongSelf = self else {
                return nil
            }
        
            // Show reset title when DND is active
            if section == .selectPeriod,
               strongSelf.pushSetting.type == .offPeriod || strongSelf.pushSetting.type == .off {
                return #localize("doNotDisturb_onPeriod_reset")
            }
        
            return nil
        
        },
        footerProvider: { [weak self] _, section -> String? in
            guard let strongSelf = self else {
                return nil
            }
        
            switch section {
            case .activeDND:
                return strongSelf.offHoursDescription
                
            case .notifyWhenMentionedSetting:
                if strongSelf.pushSetting.mentioned {
                    return #localize("doNotDisturb_mention_footer_on")
                }
                else {
                    return #localize("doNotDisturb_mention_footer_off")
                }
                
            case .notificationPlaySoundSetting:
                return #localize("doNotDisturb_notification_sound_footer")
                
            default:
                return nil
            }
        }
    )
        
    private let areOffHoursEnabled = UserSettings.shared()?.enableMasterDnd ?? false
    private lazy var offHoursDescription: String? = {
        guard areOffHoursEnabled else {
            return nil
        }
        
        let localizedInfo = #localize("doNotDisturb_offHours_info")
        let localizedDetailsFormatString = #localize("doNotDisturb_offHours_details")
    
        guard let localizedWeekdaysList = localizedOffHoursWeekdays() else {
            return localizedInfo
        }
        
        guard let localizedStartTime = localizedFormattedTime(for: UserSettings.shared()?.masterDndStartTime) else {
            return localizedInfo
        }
        
        guard let localizedEndTime = localizedFormattedTime(for: UserSettings.shared()?.masterDndEndTime) else {
            return localizedInfo
        }
        
        let localizedDetails = String.localizedStringWithFormat(
            localizedDetailsFormatString,
            localizedWeekdaysList,
            localizedStartTime,
            localizedEndTime
        )
        
        return "\(localizedInfo) \(localizedDetails)"
    }()
    
    /// The main data provider here
    private var pushSetting: PushSetting

    private let willDismiss: ((PushSetting) -> Void)?

    private var isGroup: Bool {
        pushSetting.groupIdentity != nil
    }

    // MARK: - Lifecycle
    
    /// New DND view controller for a contact or group
    ///
    /// - Parameters:
    ///   - pushSetting: The settings of a contact or group are shown and can be changed
    ///   - willDismiss: This closure will be called with the final `PushSetting` just before the view controller is
    ///                  dismissed
    init(pushSetting: PushSetting, willDismiss: ((PushSetting) -> Void)? = nil) {
        self.pushSetting = pushSetting
        self.willDismiss = willDismiss

        super.init()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(
            forName: Notification.Name(kNotificationChangedPushSetting),
            object: nil,
            queue: .main
        ) { notification in
            if let newPushSetting = notification.object as? PushSetting {
                if let identity = newPushSetting.identity, self.pushSetting.identity == identity {
                    self.pushSetting = newPushSetting
                    self.updateContent()
                }
                else if let groupIdentity = newPushSetting.groupIdentity,
                        self.pushSetting.groupIdentity == groupIdentity {
                    self.pushSetting = newPushSetting
                    self.updateContent()
                }
            }
        }

        configureNavigationBar()
        configureTableView()
        registerCells()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        updateContent()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        NotificationCenter.default.removeObserver(
            self,
            name: NSNotification.Name(kNotificationChangedPushSetting),
            object: nil
        )

        willDismiss?(pushSetting)
    }
}
    
// MARK: - Configuration

extension DoNotDisturbViewController {
    
    private func configureNavigationBar() {
        navigationBarTitle = #localize("doNotDisturb_title")
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .close,
            target: self,
            action: #selector(closeButtonTapped)
        )
    }
    
    private func configureTableView() {
        tableView.delegate = self
    }
    
    private func registerCells() {
        tableView.registerCell(ActiveDNDInfoCell.self)
        tableView.registerCell(TurnOffDNDButtonCell.self)
        tableView.registerCell(PeriodButtonCell.self)
        tableView.registerCell(NotifyWhenMentionedSettingCell.self)
        tableView.registerCell(NotificationPlaySoundSettingCell.self)
    }
}

// MARK: - Updates

extension DoNotDisturbViewController {
    
    private func updateContent() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Row>()
        
        // Always show activeDND section when off-hours are enable to display info in footer
        if pushSetting.type != .on || areOffHoursEnabled {
            snapshot.appendSections([.activeDND])
        }
        
        if pushSetting.type != .on {
            snapshot.appendItems([
                .activeDNDInfo,
                .turnOffDNDButton,
            ])
            
            // Ensure update of icon if mention setting is flipped
            snapshot.reloadItems([.activeDNDInfo])
        }
        
        snapshot.appendSections([.selectPeriod])
        snapshot.appendItems(PushSetting.PeriodOffTime.allCases.map { .periodButton(duration: $0) })
        snapshot.appendItems([.foreverButton])
        
        if isGroup {
            snapshot.appendSections([.notifyWhenMentionedSetting])
            snapshot.appendItems([.notifyWhenMentionedSetting])
            
            // Ensure that we always show the most up to date footer
            snapshot.reloadSections([.notifyWhenMentionedSetting])
        }
        
        snapshot.appendSections([.notificationPlaySoundSetting])
        snapshot.appendItems([.notificationPlaySoundSetting])
        snapshot.reloadSections([.notificationPlaySoundSetting])

        dataSource.apply(snapshot)
    }
}

// MARK: - Actions

extension DoNotDisturbViewController {
    @objc private func closeButtonTapped() {
        dismiss()
    }
    
    private func dismiss() {
        dismiss(animated: true)
    }
}

// MARK: - UITableViewDelegate

extension DoNotDisturbViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        guard let row = dataSource.itemIdentifier(for: indexPath) else {
            return nil
        }
        
        switch row {
        case .turnOffDNDButton, .periodButton(duration: _), .foreverButton:
            return indexPath
        default:
            return nil
        }
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let row = dataSource.itemIdentifier(for: indexPath) else {
            tableView.deselectRow(at: indexPath, animated: true)
            return
        }
        
        if row == .turnOffDNDButton {
            pushSetting.type = .on
        }
        else if case let .periodButton(duration: duration) = row {
            pushSetting.type = .offPeriod
            pushSetting.setPeriodOffTime(duration)
        }
        else if row == .foreverButton {
            pushSetting.type = .off
        }

        Task { @MainActor in
            await businessInjector.pushSettingManager.save(pushSetting: pushSetting, sync: true)

            dismiss()
        }

        tableView.deselectRow(at: indexPath, animated: true)
    }
}

// MARK: - Off-hours formatting helpers

extension DoNotDisturbViewController {
    
    private func localizedOffHoursWeekdays() -> String? {
        let offHoursWeekdaysArray = localizedOffHoursWeekdaysArray()
        
        guard !offHoursWeekdaysArray.isEmpty else {
            return nil
        }
        
        return ListFormatter.localizedString(byJoining: offHoursWeekdaysArray)
    }
    
    private func localizedOffHoursWeekdaysArray() -> [String] {
        // I don't know why, but `firstWeekday` is "off-by-one" compared to `weekdays`.
        // `weekdays` are always from Sun to Sat. If Sun is the first day of the week `firstWeekday` is 1.
        let firstWeekday = Calendar.current.firstWeekday
        let localizedShortWeekdays = Calendar.current.shortWeekdaySymbols
        
        // Our working days use the same offset as `firstWeekday`
        guard let weekdayOffByOneSet = UserSettings.shared()?.masterDndWorkingDays else {
            return []
        }

        // Cast indexes to `Int` and remove any `nil` values. There should be no `nil` values.
        var weekdayOffByOneIndexes = weekdayOffByOneSet.map { $0 as? Int }.compactMap(\.self)
        assert(weekdayOffByOneSet.count == weekdayOffByOneIndexes.count, "All indexes should be parsable as `Int`.")
        
        // Sort our indexes according to the current locale's `firstWeekday`
        weekdayOffByOneIndexes.sort { left, right -> Bool in
            
            // Basically shift days before the first weekday after the last weekday for our comparison
            let adjust: (Int) -> Int = {
                if $0 < firstWeekday {
                    return $0 + localizedShortWeekdays.count
                }
                
                return $0
            }
            
            let left = adjust(left)
            let right = adjust(right)
            
            return left < right
        }

        // Our indexes are sorted. Thus we can just take the strings form the `localizedShortWeekdays`
        // array. (Here we adjust for the off-by-one.)
        let weekdaysStringsLocalizedAndSorted = weekdayOffByOneIndexes.map { localizedShortWeekdays[$0 - 1] }
        
        return weekdaysStringsLocalizedAndSorted
    }
    
    private func localizedFormattedTime(for timeString: String?) -> String? {
        guard let timeString,
              let date = DateFormatter.getDate(from: timeString) else {
            return nil
        }
        
        return DateFormatter.shortStyleTimeNoDate(date)
    }
}

Messung
CHZG
849610093

¤ Dauer der Verarbeitung: 0.2 Sekunden  (vorverarbeitet am  2026-11-04) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.