Path: blob/main/Signal/src/ViewControllers/MediaGallery/Cells/AudioAllMediaPresenter.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
class AudioAllMediaPresenter: AudioPresenter {
private enum Constants {
static var filenameFont: UIFont { .dynamicTypeSubheadlineClamped }
static var bottomLineFont: UIFont { .dynamicTypeFootnoteClamped }
static var bottomInnerStackSpacing: CGFloat {
switch UIApplication.shared.preferredContentSizeCategory {
case .extraSmall, .small, .medium, .large, .extraLarge:
return 8
default:
return 4
}
}
}
let name = "AudioAllMedia"
// Required by the AudioPresenter protocol, should not be used.
let isIncoming = false
let sender: String
let size: String
let date: String
let playbackTimeLabel = CVLabel()
let playedDotContainer = ManualLayoutView(name: "playedDotContainer")
let playbackRateView: AudioMessagePlaybackRateView
let senderLabel = CVLabel()
let sizeLabel = CVLabel()
let dateLabel = CVLabel()
let dot1 = CVLabel()
let dot2 = CVLabel()
private static let middleDot = " · "
func playedColor(isIncoming: Bool) -> UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray05 : .ows_gray90
}
func unplayedColor(isIncoming: Bool) -> UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray60 : .ows_gray20
}
func thumbColor(isIncoming: Bool) -> UIColor {
return playedColor(isIncoming: true)
}
func playPauseContainerBackgroundColor(
conversationStyle: ConversationStyle,
isIncoming: Bool,
) -> UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray65 : .ows_gray05
}
func playPauseAnimationColor(isIncoming: Bool) -> UIColor {
playedColor(isIncoming: true)
}
func playedDotAnimationColor(conversationStyle: ConversationStyle, isIncoming: Bool) -> UIColor {
conversationStyle.bubbleSecondaryTextColor(isIncoming: true)
}
var bottomInnerStackSpacing: CGFloat { 0 }
let audioAttachment: AudioAttachment
let threadUniqueId: String
let audioPlaybackRate: AudioPlaybackRate
init(
sender: String,
audioAttachment: AudioAttachment,
threadUniqueId: String,
playbackRate: AudioPlaybackRate,
) {
self.threadUniqueId = threadUniqueId
self.sender = sender
self.audioPlaybackRate = playbackRate
self.audioAttachment = audioAttachment
self.size = audioAttachment.sizeString
self.date = audioAttachment.dateString
playbackRateView = AllMediaAudioMessagePlaybackRateView(
threadUniqueId: threadUniqueId,
audioAttachment: audioAttachment,
playbackRate: playbackRate,
isIncoming: true,
)
}
func configureForRendering(conversationStyle: ConversationStyle) {
let playbackTimeLabelConfig = Self.playbackTimeLabelConfig(isIncoming: true)
playbackTimeLabelConfig.applyForRendering(label: playbackTimeLabel)
playbackTimeLabel.setContentHuggingHigh()
let senderLabelConfig = Self.labelConfig_render(
text: sender,
lineBreakMode: .byTruncatingTail,
conversationStyle: conversationStyle,
)
let sizeLabelConfig = Self.labelConfig_render(text: size, conversationStyle: conversationStyle)
let dateLabelConfig = Self.labelConfig_render(text: date, conversationStyle: conversationStyle)
let dot1Config = Self.labelConfig_render(text: AudioAllMediaPresenter.middleDot, conversationStyle: conversationStyle)
let dot2Config = Self.labelConfig_render(text: AudioAllMediaPresenter.middleDot, conversationStyle: conversationStyle)
senderLabelConfig.applyForRendering(label: senderLabel)
senderLabel.setContentHuggingHigh()
sizeLabelConfig.applyForRendering(label: sizeLabel)
sizeLabel.setContentHuggingHigh()
dateLabelConfig.applyForRendering(label: dateLabel)
dateLabel.setContentHuggingHigh()
dot1Config.applyForRendering(label: dot1)
dot1.setContentHuggingHigh()
dot2Config.applyForRendering(label: dot2)
dot2.setContentHuggingHigh()
}
private static func labelConfig_render(
text: String,
lineBreakMode: NSLineBreakMode = .byWordWrapping,
conversationStyle: ConversationStyle,
) -> CVLabelConfig {
return CVLabelConfig.unstyledText(
text,
font: Constants.bottomLineFont,
textColor: conversationStyle.bubbleSecondaryTextColor(isIncoming: true),
lineBreakMode: lineBreakMode,
)
}
private var subviews: Subviews {
Subviews(
playedDotContainer: playedDotContainer,
playbackTimeLabel: playbackTimeLabel,
playbackRateView: playbackRateView,
senderLabel: senderLabel,
sizeLabel: sizeLabel,
dateLabel: dateLabel,
dot1: dot1,
dot2: dot2,
)
}
var bottomSubviews: [UIView] {
let subviews = self.subviews
return Self.bottomViewsWithSizingInfo.map { $0.view(subviews) }
}
private struct Subviews {
var playedDotContainer: UIView
var playbackTimeLabel: UIView
var playbackRateView: UIView
var senderLabel: UIView
var sizeLabel: UIView
var dateLabel: UIView
var dot1: UIView
var dot2: UIView
}
private struct SubviewConfig {
var dotSize: CGSize
var maxWidth: CGFloat
var playbackTimeLabelSize: CGSize
var playbackRateSize: CGSize
var senderSize: CGSize
var sizeSize: CGSize
var dateSize: CGSize
var dot1Size: CGSize
var dot2Size: CGSize
}
private struct ViewWithSizingInfo {
var id: String
var view: (Subviews) -> (UIView)
var subviewInfo: (SubviewConfig) -> (ManualStackSubviewInfo)
var shouldAddSubview: Bool
}
private static var bottomViewsWithSizingInfo: [ViewWithSizingInfo] = {
return [
ViewWithSizingInfo(
id: "senderLabel",
view: { $0.senderLabel },
subviewInfo: { $0.senderSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "dot1",
view: { $0.dot1 },
subviewInfo: { $0.dot1Size.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "sizeLabel",
view: { $0.sizeLabel },
subviewInfo: { $0.sizeSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "dot2",
view: { $0.dot2 },
subviewInfo: { $0.dot2Size.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "dateLabel",
view: { $0.dateLabel },
subviewInfo: { $0.dateSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "hStretchingSpacer",
view: { _ in .hStretchingSpacer() },
subviewInfo: { _ in .empty },
shouldAddSubview: false,
),
ViewWithSizingInfo(
id: "transparentSpacer1",
view: { _ in UIView.transparentSpacer() },
subviewInfo: { _ in
CGSize(width: Constants.bottomInnerStackSpacing, height: 0).asManualSubviewInfo(hasFixedSize: true)
},
shouldAddSubview: false,
),
ViewWithSizingInfo(
id: "playbackRateView",
view: { $0.playbackRateView },
subviewInfo: { $0.playbackRateSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "transparentSpacer2",
view: { _ in UIView.transparentSpacer() },
subviewInfo: { _ in
CGSize(width: Constants.bottomInnerStackSpacing, height: 0).asManualSubviewInfo(hasFixedSize: true)
},
shouldAddSubview: false,
),
ViewWithSizingInfo(
id: "playbackTimeLabel",
view: { $0.playbackTimeLabel },
subviewInfo: { $0.playbackTimeLabelSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
ViewWithSizingInfo(
id: "transparentSpacer3",
view: { _ in UIView.transparentSpacer() },
subviewInfo: { _ in
CGSize(width: 4, height: 0).asManualSubviewInfo(hasFixedSize: true)
},
shouldAddSubview: false,
),
ViewWithSizingInfo(
id: "playedDotContainer",
view: { $0.playedDotContainer },
subviewInfo: { $0.dotSize.asManualSubviewInfo(hasFixedSize: true) },
shouldAddSubview: true,
),
]
}()
func bottomSubviewGenerators(conversationStyle: ConversationStyle?) -> [SubviewGenerator] {
let makeSubviewConfig = { [unowned self] (maxWidth: CGFloat) -> SubviewConfig in
let playbackTimeLabelConfig = Self.playbackTimeLabelConfig_forMeasurement(
audioAttachment: audioAttachment,
maxWidth: maxWidth,
)
let playbackTimeLabelSize = CVText.measureLabel(config: playbackTimeLabelConfig, maxWidth: maxWidth)
let senderLabelConfig = labelConfig_forMeasurement(text: sender)
let sizeLabelConfig = labelConfig_forMeasurement(text: audioAttachment.sizeString)
let dateLabelConfig = labelConfig_forMeasurement(text: audioAttachment.dateString)
let dot1Config = labelConfig_forMeasurement(text: AudioAllMediaPresenter.middleDot)
let dot2Config = labelConfig_forMeasurement(text: AudioAllMediaPresenter.middleDot)
var senderSize = CVText.measureLabel(config: senderLabelConfig, maxWidth: maxWidth)
senderSize.width = min(senderSize.width, round(maxWidth * 0.33))
let sizeSize = CVText.measureLabel(config: sizeLabelConfig, maxWidth: maxWidth)
let dateSize = CVText.measureLabel(config: dateLabelConfig, maxWidth: maxWidth)
let dot1Size = CVText.measureLabel(config: dot1Config, maxWidth: maxWidth)
let dot2Size = CVText.measureLabel(config: dot2Config, maxWidth: maxWidth)
let playbackRateSize = AudioMessagePlaybackRateView.measure(maxWidth: maxWidth)
let dotSize = CGSize(square: 6)
let subviewConfig = SubviewConfig(
dotSize: dotSize,
maxWidth: maxWidth,
playbackTimeLabelSize: playbackTimeLabelSize,
playbackRateSize: playbackRateSize,
senderSize: senderSize,
sizeSize: sizeSize,
dateSize: dateSize,
dot1Size: dot1Size,
dot2Size: dot2Size,
)
return subviewConfig
}
var subviewConfig: SubviewConfig?
let lazySubviewConfig = { maxWidth in
if let subviewConfig {
return subviewConfig
}
let result = makeSubviewConfig(maxWidth)
subviewConfig = result
return result
}
return Self.bottomViewsWithSizingInfo.map { [unowned self] vwsi in
return SubviewGenerator(
id: vwsi.id,
measurementInfo: { vwsi.subviewInfo(lazySubviewConfig($0)) },
viewGenerator: { vwsi.view(self.subviews) },
)
}
}
private func labelConfig_forMeasurement(text: String) -> CVLabelConfig {
return CVLabelConfig.unstyledText(text, font: Constants.bottomLineFont, textColor: .Signal.label)
}
static func hasAttachmentLabel(attachment: Attachment, isVoiceMessage: Bool) -> Bool {
return !isVoiceMessage
}
func hasAttachmentLabel(attachment: Attachment, isVoiceMessage: Bool) -> Bool {
return Self.hasAttachmentLabel(attachment: attachment, isVoiceMessage: isVoiceMessage)
}
var topLabelConfig: CVLabelConfig? {
guard
hasAttachmentLabel(
attachment: audioAttachment.attachment,
isVoiceMessage: audioAttachment.isVoiceMessage,
)
else {
return nil
}
let text: String
if let fileName = audioAttachment.sourceFilename?.stripped, !fileName.isEmpty {
text = fileName
} else {
text = NSLocalizedString("GENERIC_ATTACHMENT_LABEL", comment: "A label for generic attachments.")
}
return CVLabelConfig.unstyledText(
text,
font: Constants.filenameFont,
textColor: .Signal.label,
)
}
func audioWaveform(attachmentStream: AttachmentStream?) -> Task<AudioWaveform, Error>? {
return attachmentStream?.highPriorityAudioWaveform()
}
}
class AllMediaAudioMessagePlaybackRateView: AudioMessagePlaybackRateView {
override func makeBackgroundColor() -> UIColor {
return (Theme.isDarkThemeEnabled ? UIColor.ows_white : .ows_black).withAlphaComponent(0.08)
}
override func makeTextColor() -> UIColor {
return Theme.isDarkThemeEnabled ? .ows_gray15 : .ows_gray60
}
}