mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
d3604e8115
* Announce toasts with VoiceOver Toasts are posted from a separate overlay window and never take VoiceOver focus, so their text was never spoken. UserIndicatorController now posts a high priority announcement with the title whenever a toast with readable text is submitted, including re-submissions of an already visible toast with the same ID. Modal indicators are not announced. * Remove empty-title toast announcement check Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Hide toasts from VoiceOver The toast title is now announced by UserIndicatorController when the toast is submitted, so letting VoiceOver also focus the toast itself would only read the same text twice. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
107 lines
3.6 KiB
Swift
107 lines
3.6 KiB
Swift
//
|
|
// Copyright 2025 Element Creations Ltd.
|
|
// Copyright 2022-2025 New Vector Ltd.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
// Please see LICENSE files in the repository root for full details.
|
|
//
|
|
|
|
import Combine
|
|
import SwiftUI
|
|
|
|
class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol {
|
|
private var timerCancellable: AnyCancellable?
|
|
private var displayTimes = [String: Date]()
|
|
private var delayedIndicators = Set<String>()
|
|
|
|
var nonPersistentDisplayDuration = 2.5
|
|
var minimumDisplayDuration = 0.5
|
|
|
|
@Published private(set) var activeIndicator: UserIndicator?
|
|
private(set) var indicatorQueue = [UserIndicator]() {
|
|
didSet {
|
|
activeIndicator = indicatorQueue.last
|
|
|
|
if let activeIndicator, !activeIndicator.persistent {
|
|
timerCancellable?.cancel()
|
|
timerCancellable = Task { [weak self, nonPersistentDisplayDuration] in
|
|
try await Task.sleep(for: .seconds(nonPersistentDisplayDuration))
|
|
self?.retractIndicatorWithId(activeIndicator.id)
|
|
}.asCancellable()
|
|
}
|
|
}
|
|
}
|
|
|
|
var window: UIWindow? {
|
|
didSet {
|
|
let hostingController = UIHostingController(rootView: UserIndicatorPresenter(userIndicatorController: self).statusBarHidden(ProcessInfo.isRunningUITests))
|
|
hostingController.view.backgroundColor = .clear
|
|
window?.rootViewController = hostingController
|
|
}
|
|
}
|
|
|
|
func submitIndicator(_ indicator: UserIndicator, delay: Duration?) {
|
|
if let index = indicatorQueue.firstIndex(where: { $0.id == indicator.id }) {
|
|
indicatorQueue[index] = indicator
|
|
displayTimes[indicator.id] = .now
|
|
announceIfNeeded(indicator)
|
|
} else {
|
|
if let delay {
|
|
delayedIndicators.insert(indicator.id)
|
|
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(delay.seconds))
|
|
|
|
guard delayedIndicators.contains(indicator.id) else {
|
|
return
|
|
}
|
|
|
|
enqueue(indicator: indicator)
|
|
}
|
|
} else {
|
|
enqueue(indicator: indicator)
|
|
}
|
|
}
|
|
}
|
|
|
|
func retractAllIndicators() {
|
|
for indicator in indicatorQueue {
|
|
retractIndicatorWithId(indicator.id)
|
|
}
|
|
}
|
|
|
|
func retractIndicatorWithId(_ id: String) {
|
|
delayedIndicators.remove(id)
|
|
|
|
guard let displayTime = displayTimes[id], abs(displayTime.timeIntervalSinceNow) <= minimumDisplayDuration else {
|
|
indicatorQueue.removeAll { $0.id == id }
|
|
return
|
|
}
|
|
|
|
Task {
|
|
try? await Task.sleep(for: .seconds(minimumDisplayDuration))
|
|
indicatorQueue.removeAll { $0.id == id }
|
|
displayTimes[id] = nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func enqueue(indicator: UserIndicator) {
|
|
retractIndicatorWithId(indicator.id)
|
|
indicatorQueue.append(indicator)
|
|
displayTimes[indicator.id] = .now
|
|
announceIfNeeded(indicator)
|
|
}
|
|
|
|
private func announceIfNeeded(_ indicator: UserIndicator) {
|
|
guard case .toast = indicator.type else {
|
|
return
|
|
}
|
|
|
|
var announcement = AttributedString(indicator.title)
|
|
announcement.accessibilitySpeechAnnouncementPriority = .high
|
|
AccessibilityNotification.Announcement(announcement).post()
|
|
}
|
|
}
|