321 lines
11 KiB
Swift
321 lines
11 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
public class CVComponentUnreadIndicator: CVComponentBase, CVRootComponent {
|
|
|
|
public var componentKey: CVComponentKey { .unreadIndicator }
|
|
|
|
public var cellReuseIdentifier: CVCellReuseIdentifier {
|
|
CVCellReuseIdentifier.unreadIndicator
|
|
}
|
|
|
|
public let isDedicatedCell = true
|
|
|
|
override init(itemModel: CVItemModel) {
|
|
super.init(itemModel: itemModel)
|
|
}
|
|
|
|
public func configureCellRootComponent(
|
|
cellView: UIView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate,
|
|
messageSwipeActionState: CVMessageSwipeActionState,
|
|
componentView: CVComponentView,
|
|
) {
|
|
Self.configureCellRootComponent(
|
|
rootComponent: self,
|
|
cellView: cellView,
|
|
cellMeasurement: cellMeasurement,
|
|
componentDelegate: componentDelegate,
|
|
componentView: componentView,
|
|
)
|
|
}
|
|
|
|
public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
|
|
CVComponentViewUnreadIndicator()
|
|
}
|
|
|
|
public func configureForRendering(
|
|
componentView: CVComponentView,
|
|
cellMeasurement: CVCellMeasurement,
|
|
componentDelegate: CVComponentDelegate,
|
|
) {
|
|
guard let componentView = componentView as? CVComponentViewUnreadIndicator else {
|
|
owsFailDebug("Unexpected componentView.")
|
|
return
|
|
}
|
|
|
|
let themeHasChanged = conversationStyle.isDarkThemeEnabled != componentView.isDarkThemeEnabled
|
|
let hasWallpaper = conversationStyle.hasWallpaper
|
|
let wallpaperModeHasChanged = hasWallpaper != componentView.hasWallpaper
|
|
|
|
let isReusing = (
|
|
componentView.rootView.superview != nil &&
|
|
!themeHasChanged &&
|
|
!wallpaperModeHasChanged,
|
|
)
|
|
|
|
if !isReusing {
|
|
componentView.reset(resetReusableState: true)
|
|
}
|
|
|
|
componentView.isDarkThemeEnabled = conversationStyle.isDarkThemeEnabled
|
|
componentView.hasWallpaper = hasWallpaper
|
|
|
|
let outerStack = componentView.outerStack
|
|
let innerStack = componentView.innerStack
|
|
let titleLabel = componentView.titleLabel
|
|
let strokeViewLeading = componentView.strokeViewLeading
|
|
let strokeViewTrailing = componentView.strokeViewTrailing
|
|
|
|
titleLabelConfig.applyForRendering(label: titleLabel)
|
|
|
|
if isReusing {
|
|
innerStack.configureForReuse(
|
|
config: innerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_innerStack,
|
|
)
|
|
outerStack.configureForReuse(
|
|
config: outerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_outerStack,
|
|
)
|
|
} else {
|
|
outerStack.reset()
|
|
|
|
let visualEffectView: UIVisualEffectView
|
|
if #available(iOS 26, *) {
|
|
let glassEffectView = UIVisualEffectView(effect: UIGlassEffect(style: .regular))
|
|
glassEffectView.contentView.addSubview(titleLabel)
|
|
glassEffectView.cornerConfiguration = .capsule()
|
|
visualEffectView = glassEffectView
|
|
} else {
|
|
let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
|
blurEffectView.contentView.addSubview(titleLabel)
|
|
blurEffectView.layer.masksToBounds = true
|
|
visualEffectView = blurEffectView
|
|
}
|
|
titleLabel.autoPinEdgesToSuperviewEdges(with: Self.titleLabelMargins)
|
|
|
|
let wrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(visualEffectView)
|
|
if #unavailable(iOS 26) {
|
|
wrapper.addLayoutBlock { view in
|
|
visualEffectView.layer.cornerRadius = view.bounds.size.smallerAxis / 2
|
|
}
|
|
}
|
|
|
|
innerStack.reset()
|
|
innerStack.configure(
|
|
config: innerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_innerStack,
|
|
subviews: [wrapper],
|
|
)
|
|
|
|
let strokeViewStyle: StrokeView.Style = hasWallpaper ? .double : .single
|
|
strokeViewLeading.style = strokeViewStyle
|
|
strokeViewTrailing.style = strokeViewStyle
|
|
|
|
outerStack.configure(
|
|
config: outerStackConfig,
|
|
cellMeasurement: cellMeasurement,
|
|
measurementKey: Self.measurementKey_outerStack,
|
|
subviews: [
|
|
strokeViewLeading,
|
|
innerStack,
|
|
strokeViewTrailing,
|
|
],
|
|
)
|
|
}
|
|
}
|
|
|
|
private var titleLabelConfig: CVLabelConfig {
|
|
CVLabelConfig.unstyledText(
|
|
OWSLocalizedString(
|
|
"MESSAGES_VIEW_UNREAD_INDICATOR",
|
|
comment: "Indicator that separates read from unread messages.",
|
|
),
|
|
font: UIFont.dynamicTypeFootnote.medium(),
|
|
textColor: Theme.primaryTextColor,
|
|
numberOfLines: 0,
|
|
lineBreakMode: .byTruncatingTail,
|
|
textAlignment: .center,
|
|
)
|
|
}
|
|
|
|
private static var titleLabelMargins = UIEdgeInsets(hMargin: 12, vMargin: 3)
|
|
|
|
private var outerStackConfig: CVStackViewConfig {
|
|
let cellLayoutMargins = UIEdgeInsets(
|
|
top: 8,
|
|
leading: conversationStyle.fullWidthGutterLeading,
|
|
bottom: 8,
|
|
trailing: conversationStyle.fullWidthGutterTrailing,
|
|
)
|
|
return CVStackViewConfig(
|
|
axis: .horizontal,
|
|
alignment: .center,
|
|
spacing: 6,
|
|
layoutMargins: cellLayoutMargins,
|
|
)
|
|
}
|
|
|
|
private var innerStackConfig: CVStackViewConfig {
|
|
CVStackViewConfig(
|
|
axis: .vertical,
|
|
alignment: .center,
|
|
spacing: 0,
|
|
layoutMargins: .zero,
|
|
)
|
|
}
|
|
|
|
private static let measurementKey_outerStack = "CVComponentUnreadIndicator.measurementKey_outerStack"
|
|
private static let measurementKey_innerStack = "CVComponentUnreadIndicator.measurementKey_innerStack"
|
|
|
|
public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
|
|
owsAssertDebug(maxWidth > 0)
|
|
|
|
let availableWidth = max(0, maxWidth - (Self.titleLabelMargins.totalWidth + outerStackConfig.layoutMargins.totalWidth))
|
|
let labelSize = CVText.measureLabel(config: titleLabelConfig, maxWidth: availableWidth) + Self.titleLabelMargins.asSize
|
|
|
|
let strokeSize = CGSize(width: 0, height: 2)
|
|
|
|
let labelInfo = labelSize.asManualSubviewInfo
|
|
let innerStackMeasurement = ManualStackView.measure(
|
|
config: innerStackConfig,
|
|
measurementBuilder: measurementBuilder,
|
|
measurementKey: Self.measurementKey_innerStack,
|
|
subviewInfos: [labelInfo],
|
|
)
|
|
|
|
let strokeInfo = strokeSize.asManualSubviewInfo(hasFixedHeight: true)
|
|
let innerStackInfo = innerStackMeasurement.measuredSize.asManualSubviewInfo(hasFixedWidth: true)
|
|
let hStackSubviewInfos = [
|
|
strokeInfo,
|
|
innerStackInfo,
|
|
strokeInfo,
|
|
]
|
|
let hStackMeasurement = ManualStackView.measure(
|
|
config: outerStackConfig,
|
|
measurementBuilder: measurementBuilder,
|
|
measurementKey: Self.measurementKey_outerStack,
|
|
subviewInfos: hStackSubviewInfos,
|
|
maxWidth: maxWidth,
|
|
)
|
|
return hStackMeasurement.measuredSize
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// Used for rendering some portion of an Conversation View item.
|
|
// It could be the entire item or some part thereof.
|
|
public class CVComponentViewUnreadIndicator: NSObject, CVComponentView {
|
|
|
|
fileprivate let outerStack = ManualStackView(name: "unreadIndicator.outerStack")
|
|
fileprivate let innerStack = ManualStackView(name: "unreadIndicator.innerStack")
|
|
|
|
fileprivate let titleLabel = CVLabel()
|
|
|
|
fileprivate var hasWallpaper = false
|
|
fileprivate var isDarkThemeEnabled = false
|
|
|
|
fileprivate let strokeViewLeading = StrokeView()
|
|
fileprivate let strokeViewTrailing = StrokeView()
|
|
|
|
public var isDedicatedCellView = false
|
|
|
|
public var rootView: UIView {
|
|
outerStack
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public func setIsCellVisible(_ isCellVisible: Bool) {}
|
|
|
|
public func reset() {
|
|
reset(resetReusableState: false)
|
|
}
|
|
|
|
public func reset(resetReusableState: Bool) {
|
|
owsAssertDebug(isDedicatedCellView)
|
|
|
|
titleLabel.text = nil
|
|
|
|
if resetReusableState {
|
|
outerStack.reset()
|
|
innerStack.reset()
|
|
|
|
hasWallpaper = false
|
|
isDarkThemeEnabled = false
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate class StrokeView: ManualLayoutView {
|
|
enum Style {
|
|
case single
|
|
case double
|
|
}
|
|
|
|
var style: Style = .single {
|
|
didSet {
|
|
updateStokeStyle()
|
|
}
|
|
}
|
|
|
|
private let topStrokeView = UIView()
|
|
private let middleStrokeView = UIView()
|
|
private let bottomStrokeView = UIView()
|
|
|
|
init() {
|
|
super.init(name: "StrokeView")
|
|
|
|
clipsToBounds = true
|
|
|
|
topStrokeView.backgroundColor = UIColor(white: 0, alpha: 0.32)
|
|
middleStrokeView.backgroundColor = UIColor.Signal.quaternaryLabel
|
|
bottomStrokeView.backgroundColor = UIColor(white: 1, alpha: 0.16)
|
|
|
|
addSubview(topStrokeView)
|
|
addSubview(middleStrokeView)
|
|
addSubview(bottomStrokeView)
|
|
|
|
addDefaultLayoutBlock()
|
|
updateStokeStyle()
|
|
}
|
|
|
|
private func addDefaultLayoutBlock() {
|
|
addLayoutBlock { [weak self] _ in
|
|
guard let self else { return }
|
|
|
|
let strokeViewSize = CGSize(width: self.bounds.width, height: 1)
|
|
|
|
self.topStrokeView.frame = CGRect(
|
|
origin: CGPoint(x: self.bounds.minX, y: self.bounds.midY - strokeViewSize.height),
|
|
size: strokeViewSize,
|
|
)
|
|
self.middleStrokeView.frame = CGRect(
|
|
origin: CGPoint(x: self.bounds.minX, y: self.bounds.midY - strokeViewSize.height / 2),
|
|
size: strokeViewSize,
|
|
)
|
|
self.bottomStrokeView.frame = CGRect(
|
|
origin: CGPoint(x: self.bounds.minX, y: self.bounds.midY),
|
|
size: strokeViewSize,
|
|
)
|
|
}
|
|
}
|
|
|
|
private func updateStokeStyle() {
|
|
topStrokeView.isHidden = style == .single
|
|
bottomStrokeView.isHidden = style == .single
|
|
middleStrokeView.isHidden = style == .double
|
|
}
|
|
}
|
|
}
|