Path: blob/main/Signal/ConversationView/Components/CVComponentSenderName.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
public import SignalUI
public class CVComponentSenderName: CVComponentBase, CVComponent {
public var componentKey: CVComponentKey { .senderName }
private let state: CVComponentState.SenderName
private var senderName: NSAttributedString {
let padding = " "
if let labelString = state.memberLabel {
return state.senderName + padding + labelString
} else {
return state.senderName
}
}
private var senderNameColor: UIColor { state.senderNameColor }
private var memberLabel: String? { state.memberLabel }
init(itemModel: CVItemModel, senderNameState: CVComponentState.SenderName) {
self.state = senderNameState
super.init(itemModel: itemModel)
}
public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
CVComponentViewSenderName()
}
override public func wallpaperBlurView(componentView: CVComponentView) -> CVWallpaperBlurView? {
guard let componentView = componentView as? CVComponentViewSenderName else {
owsFailDebug("Unexpected componentView.")
return nil
}
return componentView.wallpaperBlurView
}
private var bodyTextColor: UIColor {
guard let message = interaction as? TSMessage else {
return .black
}
return conversationStyle.bubbleTextColor(message: message)
}
public func configureForRendering(
componentView componentViewParam: CVComponentView,
cellMeasurement: CVCellMeasurement,
componentDelegate: CVComponentDelegate,
) {
guard let componentView = componentViewParam as? CVComponentViewSenderName else {
owsFailDebug("Unexpected componentView.")
componentViewParam.reset()
return
}
let outerStack = componentView.outerStack
let innerStack = componentView.innerStack
outerStack.reset()
innerStack.reset()
if isBorderlessWithWallpaper {
owsAssertDebug(isIncoming)
let bubbleView: UIView
if conversationStyle.hasWallpaper {
let wallpaperBlurView = componentView.ensureWallpaperBlurView()
configureWallpaperBlurView(
wallpaperBlurView: wallpaperBlurView,
componentDelegate: componentDelegate,
bubbleConfig: BubbleConfiguration(
corners: .capsule(),
stroke: ConversationStyle.bubbleStroke(isDarkThemeEnabled: isDarkThemeEnabled),
),
)
bubbleView = wallpaperBlurView
} else {
let chatColorView = componentView.chatColorView
chatColorView.configure(
value: conversationStyle.bubbleChatColorIncoming,
referenceView: componentDelegate.view,
bubbleConfig: BubbleConfiguration(corners: .capsule()),
)
bubbleView = chatColorView
}
innerStack.addSubviewToFillSuperviewEdges(bubbleView)
}
if let memberLabel = state.memberLabel {
// Finds the first or last occurance of the member label.
// Since member label is appended to the end of the string, this
// will work even if member label is a substring of sender name or equal to sender name.
let range = (senderName.string as NSString).range(of: memberLabel, options: .backwards)
componentView.label = CVCapsuleLabel(
attributedText: senderName,
textColor: senderNameColor,
font: UIFont.dynamicTypeFootnote.semibold(),
highlightRange: range,
highlightFont: .dynamicTypeFootnote,
axLabelPrefix: nil, // handled separately in CVItemViewState
presentationContext: .messageBubbleRegular,
signalSymbolRange: nil,
onTap: nil,
)
} else {
labelConfig.applyForRendering(label: componentView.label)
}
var subviews: [UIView] = []
subviews.append(componentView.label)
innerStack.configure(
config: innerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerStack,
subviews: subviews,
)
outerStack.configure(
config: outerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_outerStack,
subviews: [innerStack],
)
}
private var isBorderlessWithWallpaper: Bool {
return isBorderless && conversationStyle.hasWallpaper
}
private var labelConfig: CVLabelConfig {
let font = UIFont.dynamicTypeFootnote.semibold()
return CVLabelConfig(
text: .attributedText(senderName),
displayConfig: .forUnstyledText(font: font, textColor: senderNameColor),
font: font,
textColor: senderNameColor,
numberOfLines: 0,
lineBreakMode: .byWordWrapping,
)
}
private var outerStackConfig: CVStackViewConfig {
CVStackViewConfig(
axis: .vertical,
alignment: .leading,
spacing: 0,
layoutMargins: .zero,
)
}
private var innerStackConfig: CVStackViewConfig {
let layoutMargins: UIEdgeInsets = (
isBorderlessWithWallpaper
? UIEdgeInsets(hMargin: 12, vMargin: 3)
: .zero,
)
return CVStackViewConfig(
axis: .horizontal,
alignment: .center,
spacing: 4,
layoutMargins: layoutMargins,
)
}
private static let measurementKey_outerStack = "CVComponentSenderName.measurementKey_outerStack"
private static let measurementKey_innerStack = "CVComponentSenderName.measurementKey_innerStack"
public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
owsAssertDebug(maxWidth > 0)
let maxWidth = maxWidth - (
outerStackConfig.layoutMargins.totalWidth +
innerStackConfig.layoutMargins.totalWidth
)
var subviewInfos: [ManualStackSubviewInfo] = []
let labelSize: CGSize
if let memberLabel = state.memberLabel {
let range = (senderName.string as NSString).range(of: memberLabel, options: .backwards)
labelSize = CVCapsuleLabel.measureLabel(
attributedText: senderName,
font: UIFont.dynamicTypeFootnote.semibold(),
highlightRange: range,
highlightFont: .dynamicTypeFootnote,
presentationContext: .messageBubbleRegular,
maxWidth: maxWidth,
signalSymbolRange: nil,
)
} else {
labelSize = CVText.measureLabel(config: labelConfig, maxWidth: maxWidth)
}
let labelInfo = labelSize.asManualSubviewInfo
subviewInfos.insert(labelInfo, at: 0)
let innerStackMeasurement = ManualStackView.measure(
config: innerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_innerStack,
subviewInfos: subviewInfos,
)
let innerStackInfo = innerStackMeasurement.measuredSize.asManualSubviewInfo
let outerStackMeasurement = ManualStackView.measure(
config: outerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_outerStack,
subviewInfos: [innerStackInfo],
maxWidth: maxWidth,
)
return outerStackMeasurement.measuredSize
}
// MARK: -
// Used for rendering some portion of an Conversation View item.
// It could be the entire item or some part thereof.
public class CVComponentViewSenderName: NSObject, CVComponentView {
fileprivate var label = UILabel()
fileprivate let outerStack = ManualStackView(name: "CVComponentViewSenderName.outerStack")
fileprivate let innerStack = ManualStackView(name: "CVComponentViewSenderName.innerStack")
// Bubble view when there is no chat wallpaper.
fileprivate let chatColorView = CVColorOrGradientView()
// Bubble view when there is a chat wallpaper.
fileprivate var wallpaperBlurView: CVWallpaperBlurView?
fileprivate func ensureWallpaperBlurView() -> CVWallpaperBlurView {
if let wallpaperBlurView {
return wallpaperBlurView
}
let wallpaperBlurView = CVWallpaperBlurView()
self.wallpaperBlurView = wallpaperBlurView
return wallpaperBlurView
}
public var isDedicatedCellView = false
public var rootView: UIView {
outerStack
}
public func setIsCellVisible(_ isCellVisible: Bool) {}
public func reset() {
outerStack.reset()
innerStack.reset()
chatColorView.reset()
chatColorView.removeFromSuperview()
wallpaperBlurView?.removeFromSuperview()
label.text = nil
}
}
}