diff --git a/Signal/src/ViewControllers/AppSettings/Payments/ArchivedPaymentHistoryItem.swift b/Signal/src/ViewControllers/AppSettings/Payments/ArchivedPaymentHistoryItem.swift index cd43bbfec7..83c81a36e1 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/ArchivedPaymentHistoryItem.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/ArchivedPaymentHistoryItem.swift @@ -135,6 +135,10 @@ struct ArchivedPaymentHistoryItem: PaymentsHistoryItem { return archivedPayment.statusDescription(isOutgoing: isOutgoing) } + func statusDescriptionForAccessibility(isLongForm: Bool) -> String? { + return statusDescription(isLongForm: isLongForm) + } + /// Read status is only tracked on TSPaymentModels, so there's not really anything to do here. func markAsRead(tx: SignalServiceKit.DBWriteTransaction) { } diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentHistoryItem.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentHistoryItem.swift index 7d598553ec..650165a052 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentHistoryItem.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentHistoryItem.swift @@ -44,6 +44,8 @@ public protocol PaymentsHistoryItem { func statusDescription(isLongForm: Bool) -> String? + func statusDescriptionForAccessibility(isLongForm: Bool) -> String? + func markAsRead(tx: DBWriteTransaction) func reload(tx: DBReadTransaction) -> Self? diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift index 6ddf0a5cc8..6020da59ba 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsDetailViewController.swift @@ -289,6 +289,7 @@ class PaymentsDetailViewController: OWSTableViewController2, DatabaseChangeDeleg comment: "Label for the transaction status in the payment details view in the app settings.", ), bottomText: statusMessage, + bottomAccessibilityText: paymentItem.statusDescriptionForAccessibility(isLongForm: true), useFailedColor: paymentItem.isFailed, )) } @@ -374,6 +375,7 @@ class PaymentsDetailViewController: OWSTableViewController2, DatabaseChangeDeleg private func buildStatusItem( topText: String, bottomText: String, + bottomAccessibilityText: String? = nil, useFailedColor: Bool = false, ) -> OWSTableItem { OWSTableItem( @@ -387,6 +389,7 @@ class PaymentsDetailViewController: OWSTableViewController2, DatabaseChangeDeleg let bottomLabel = UILabel() bottomLabel.text = bottomText + bottomLabel.accessibilityLabel = bottomAccessibilityText bottomLabel.textColor = .Signal.secondaryLabel bottomLabel.font = UIFont.dynamicTypeFootnoteClamped bottomLabel.numberOfLines = 0 diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift index ba9355723c..f87c481303 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsSettingsViewController.swift @@ -500,7 +500,8 @@ class PaymentsSettingsViewController: OWSTableViewController2, PaymentsHistoryDa ) if let balanceConversionText = Self.buildBalanceConversionText(paymentBalance: paymentBalance) { - conversionLabel.text = balanceConversionText + conversionLabel.text = balanceConversionText.display + conversionLabel.accessibilityLabel = balanceConversionText.accessibility } else { conversionStack.alpha = 0 } @@ -598,7 +599,7 @@ class PaymentsSettingsViewController: OWSTableViewController2, PaymentsHistoryDa return button } - private static func buildBalanceConversionText(paymentBalance: PaymentBalance) -> String? { + private static func buildBalanceConversionText(paymentBalance: PaymentBalance) -> (display: String, accessibility: String)? { let localCurrencyCode = SSKEnvironment.shared.paymentsCurrenciesRef.currentCurrencyCode guard let currencyConversionInfo = SSKEnvironment.shared.paymentsCurrenciesRef.conversionInfo(forCurrencyCode: localCurrencyCode) else { return nil @@ -619,11 +620,14 @@ class PaymentsSettingsViewController: OWSTableViewController2, PaymentsHistoryDa // // It is sufficient to format as a time, currency conversions go stale in less than a day. let conversionFreshnessString = DateUtil.formatDateAsTime(currencyConversionInfo.conversionDate) + let conversionFreshnessAccessibilityString = DateUtil.formatDateAsTimeForAccessibility(currencyConversionInfo.conversionDate) let formatString = OWSLocalizedString( "SETTINGS_PAYMENTS_BALANCE_CONVERSION_FORMAT", comment: "Format string for the 'local balance converted into local currency' indicator. Embeds: {{ %1$@ the local balance in the local currency, %2$@ the local currency code, %3$@ the date the currency conversion rate was obtained. }}..", ) - return String.nonPluralLocalizedStringWithFormat(formatString, fiatAmountString, localCurrencyCode, conversionFreshnessString) + let display = String.nonPluralLocalizedStringWithFormat(formatString, fiatAmountString, localCurrencyCode, conversionFreshnessString) + let accessibility = String.nonPluralLocalizedStringWithFormat(formatString, fiatAmountString, localCurrencyCode, conversionFreshnessAccessibilityString) + return (display: display, accessibility: accessibility) } private func configureHistorySection( diff --git a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewUtils.swift b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewUtils.swift index aed6271d9d..c088664a19 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewUtils.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/PaymentsViewUtils.swift @@ -357,6 +357,15 @@ extension TSPaymentModel { return result } + func statusDescriptionForAccessibility(isLongForm: Bool) -> String { + var result = statusDescription(isLongForm: isLongForm) + // Replace the appended date with the accessibility-friendly version. + result = String(result.dropLast(Self.formatDate(sortDate, isLongForm: isLongForm).count + 1)) + result.append(" ") + result.append(Self.formatDateForAccessibility(sortDate, isLongForm: isLongForm)) + return result + } + static func formatDate(_ date: Date, isLongForm: Bool) -> String { if isLongForm { return statusDateTimeLongFormatter.string(from: date) @@ -365,6 +374,17 @@ extension TSPaymentModel { } } + static func formatDateForAccessibility(_ date: Date, isLongForm: Bool) -> String { + if isLongForm { + // Replace only the time portion with the spoken form; keep the date portion visual. + let datePart = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .none) + let timePart = DateUtil.formatDateAsTimeForAccessibility(date) + return "\(datePart) \(timePart)" + } else { + return statusDateShortFormatter.string(from: date) + } + } + private static func description( forFailure failure: TSPaymentFailure, isIncoming: Bool, diff --git a/Signal/src/ViewControllers/AppSettings/Payments/TSPaymentModelHistoryItem.swift b/Signal/src/ViewControllers/AppSettings/Payments/TSPaymentModelHistoryItem.swift index a1e7aae508..4ec028bb13 100644 --- a/Signal/src/ViewControllers/AppSettings/Payments/TSPaymentModelHistoryItem.swift +++ b/Signal/src/ViewControllers/AppSettings/Payments/TSPaymentModelHistoryItem.swift @@ -123,6 +123,10 @@ public struct PaymentsHistoryModelItem: PaymentsHistoryItem { paymentModel.statusDescription(isLongForm: isLongForm) } + public func statusDescriptionForAccessibility(isLongForm: Bool) -> String? { + paymentModel.statusDescriptionForAccessibility(isLongForm: isLongForm) + } + public func markAsRead(tx: DBWriteTransaction) { PaymentUtils.markPaymentAsRead(paymentModel, transaction: tx) } diff --git a/SignalServiceKit/Util/DateUtil.swift b/SignalServiceKit/Util/DateUtil.swift index d77e609b5e..d104cb1873 100644 --- a/SignalServiceKit/Util/DateUtil.swift +++ b/SignalServiceKit/Util/DateUtil.swift @@ -25,6 +25,13 @@ public class DateUtil { return formatter }() + private static let accessibilityTimeFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .spellOut + formatter.allowedUnits = [.hour, .minute] + return formatter + }() + public static let weekdayFormatter: DateFormatter = { let formatter = DateFormatter() formatter.setLocalizedDateFormatFromTemplate("EEEE") @@ -170,6 +177,11 @@ public class DateUtil { return timeFormatter.string(from: date) } + public static func formatDateAsTimeForAccessibility(_ date: Date) -> String { + let components = Calendar.current.dateComponents([.hour, .minute], from: date) + return accessibilityTimeFormatter.string(from: components) ?? timeFormatter.string(from: date) + } + // MARK: Formatting for UI // We might receive a message "from the future" due to a bug or @@ -206,6 +218,10 @@ public class DateUtil { let format = shouldUseLongFormat ? longFormat : shortFormat return String.localizedStringWithFormat(format, minutesDiff) } else { + if shouldUseLongFormat { + let components = Calendar.current.dateComponents([.hour, .minute], from: date) + return accessibilityTimeFormatter.string(from: components) ?? timeFormatter.string(from: date) + } return timeFormatter.string(from: date) } }