// // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // import SignalServiceKit import SignalUI public class CVComponentDateHeader: CVComponentBase, CVRootComponent { public var componentKey: CVComponentKey { .dateHeader } public var cellReuseIdentifier: CVCellReuseIdentifier { CVCellReuseIdentifier.dateHeader } public let isDedicatedCell = true struct State: Equatable { let text: String } private let dateHeaderState: State init(itemModel: CVItemModel, dateHeaderState: State) { self.dateHeaderState = dateHeaderState 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 { CVComponentViewDateHeader() } public override func wallpaperBlurView(componentView: CVComponentView) -> CVWallpaperBlurView? { guard let componentView = componentView as? CVComponentViewDateHeader else { owsFailDebug("Unexpected componentView.") return nil } return componentView.contentViewDefault?.wallpaperBlurView } public override func apply(layoutAttributes: CVCollectionViewLayoutAttributes, componentView: CVComponentView) { guard let componentView = componentView as? CVComponentViewDateHeader else { owsFailDebug("Unexpected componentView.") return } guard let doubleContentView = componentView.doubleContentView else { return } doubleContentView.normalView.isHidden = layoutAttributes.isStickyHeader doubleContentView.stickyView.isHidden = !layoutAttributes.isStickyHeader } fileprivate struct DoubleContentView { let normalView: UIView let stickyView: UIView } public func configureForRendering(componentView: CVComponentView, cellMeasurement: CVCellMeasurement, componentDelegate: CVComponentDelegate) { guard let componentView = componentView as? CVComponentViewDateHeader else { owsFailDebug("Unexpected componentView.") return } let themeHasChanged = 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 = isDarkThemeEnabled componentView.hasWallpaper = hasWallpaper let outerStack = componentView.outerStack let doubleContentWrapper = componentView.doubleContentWrapper let blurBackgroundColor: UIColor = { if componentDelegate.isConversationPreview { return isDarkThemeEnabled ? .ows_blackAlpha40 : .ows_whiteAlpha60 } else { return isDarkThemeEnabled ? UIColor(rgbHex: 0x1B1B1B) : UIColor(rgbHex: 0xFAFAFA) } }() if isReusing { outerStack.configureForReuse(config: outerStackConfig, cellMeasurement: cellMeasurement, measurementKey: Self.measurementKey_outerStack) let contentViewDefault = componentView.contentViewDefault let contentViewForBlur = componentView.contentViewForBlur contentViewDefault?.configure(componentView: componentView, cellMeasurement: cellMeasurement, componentDelegate: componentDelegate, hasWallpaper: hasWallpaper, titleLabelConfig: titleLabelConfig, innerStackConfig: innerStackConfig, isReusing: true) contentViewForBlur?.configure(blurBackgroundColor: blurBackgroundColor, titleLabelConfig: titleLabelConfig, innerStackConfig: innerStackConfig, isReusing: true) } else { outerStack.reset() doubleContentWrapper.reset() let contentView: UIView = { func buildContentViewWithBlur() -> UIView { let contentView = componentView.ensureContentViewForBlur() contentView.configure(blurBackgroundColor: blurBackgroundColor, titleLabelConfig: titleLabelConfig, innerStackConfig: innerStackConfig, isReusing: false) return contentView.rootView } func buildContentViewDefault() -> UIView { let contentView = componentView.ensureContentViewDefault() contentView.configure(componentView: componentView, cellMeasurement: cellMeasurement, componentDelegate: componentDelegate, hasWallpaper: hasWallpaper, titleLabelConfig: titleLabelConfig, innerStackConfig: innerStackConfig, isReusing: false) return contentView.rootView } if componentDelegate.isConversationPreview { return buildContentViewWithBlur() } else if hasWallpaper { return buildContentViewDefault() } else { let blurContentView = buildContentViewWithBlur() let defaultContentView = buildContentViewDefault() doubleContentWrapper.addSubviewToFillSuperviewEdges(blurContentView) doubleContentWrapper.addSubviewToFillSuperviewEdges(defaultContentView) componentView.doubleContentView = DoubleContentView(normalView: defaultContentView, stickyView: blurContentView) return doubleContentWrapper } }() outerStack.configure(config: outerStackConfig, cellMeasurement: cellMeasurement, measurementKey: Self.measurementKey_outerStack, subviews: [ contentView ]) } componentView.rootView.accessibilityLabel = titleLabelConfig.text.accessibilityDescription componentView.rootView.isAccessibilityElement = true componentView.rootView.accessibilityTraits = .header } static func buildState(interaction: TSInteraction) -> State { let date = Date(millisecondsSince1970: interaction.timestamp) let text = DateUtil.formatDateHeaderForCVC(date) return State(text: text) } private var titleLabelConfig: CVLabelConfig { return CVLabelConfig( text: .text(dateHeaderState.text), displayConfig: .forUnstyledText(font: .dynamicTypeFootnote.semibold(), textColor: Theme.secondaryTextAndIconColor), font: UIFont.dynamicTypeFootnote.semibold(), textColor: Theme.secondaryTextAndIconColor, lineBreakMode: .byTruncatingTail, textAlignment: .center ) } private var outerStackConfig: CVStackViewConfig { CVStackViewConfig(axis: .vertical, alignment: .center, spacing: 0, layoutMargins: UIEdgeInsets(top: 0, leading: conversationStyle.headerGutterLeading, bottom: 0, trailing: conversationStyle.headerGutterTrailing)) } private var innerStackConfig: CVStackViewConfig { CVStackViewConfig(axis: .vertical, alignment: .center, spacing: 0, layoutMargins: UIEdgeInsets(hMargin: 10, vMargin: 4)) } fileprivate static let measurementKey_outerStack = "CVComponentDateHeader.measurementKey_outerStack" fileprivate static let measurementKey_innerStack = "CVComponentDateHeader.measurementKey_innerStack" public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize { owsAssertDebug(maxWidth > 0) let availableWidth = max(0, maxWidth - (innerStackConfig.layoutMargins.totalWidth + outerStackConfig.layoutMargins.totalWidth)) let labelSize = CVText.measureLabel(config: titleLabelConfig, maxWidth: availableWidth) let labelInfo = labelSize.asManualSubviewInfo let innerStackMeasurement = ManualStackView.measure(config: innerStackConfig, measurementBuilder: measurementBuilder, measurementKey: Self.measurementKey_innerStack, subviewInfos: [ labelInfo ]) 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 CVComponentViewDateHeader: NSObject, CVComponentView { fileprivate let outerStack = ManualStackView(name: "dateHeader.outerStackView") fileprivate let doubleContentWrapper = ManualLayoutView(name: "dateHeader.doubleContentWrapper") fileprivate var contentViewDefault: ContentViewDefault? fileprivate func ensureContentViewDefault() -> ContentViewDefault { if let contentViewDefault = self.contentViewDefault { return contentViewDefault } let contentViewDefault = ContentViewDefault() self.contentViewDefault = contentViewDefault return contentViewDefault } fileprivate var contentViewForBlur: ContentViewForBlur? fileprivate func ensureContentViewForBlur() -> ContentViewForBlur { if let contentViewForBlur = self.contentViewForBlur { return contentViewForBlur } let contentViewForBlur = ContentViewForBlur() self.contentViewForBlur = contentViewForBlur return contentViewForBlur } fileprivate var hasWallpaper = false fileprivate var isDarkThemeEnabled = false public var isDedicatedCellView = false fileprivate var doubleContentView: DoubleContentView? public var rootView: UIView { outerStack } // MARK: - public func setIsCellVisible(_ isCellVisible: Bool) {} public func reset() { reset(resetReusableState: false) } public func reset(resetReusableState: Bool) { owsAssertDebug(isDedicatedCellView) contentViewDefault?.reset(resetReusableState: resetReusableState) contentViewForBlur?.reset(resetReusableState: resetReusableState) if resetReusableState { outerStack.reset() doubleContentWrapper.reset() hasWallpaper = false isDarkThemeEnabled = false doubleContentView = nil } } } } // MARK: - private class ContentViewForBlur { private let titleLabel = CVLabel() private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) private let blurOverlay = UIView() private let wrapper: UIView private var layoutConstraints = [NSLayoutConstraint]() var rootView: UIView { wrapper } init() { blurView.contentView.addSubview(blurOverlay) blurOverlay.autoPinEdgesToSuperviewEdges() blurView.clipsToBounds = true let wrapper = ManualLayoutView.wrapSubviewUsingIOSAutoLayout(blurView) let blurView = self.blurView wrapper.addLayoutBlock { view in blurView.layer.cornerRadius = view.frame.size.smallerAxis * 0.5 } self.wrapper = wrapper } func configure(blurBackgroundColor: UIColor, titleLabelConfig: CVLabelConfig, innerStackConfig: CVStackViewConfig, isReusing: Bool) { if !isReusing { reset(resetReusableState: true) } titleLabelConfig.applyForRendering(label: titleLabel) blurOverlay.backgroundColor = blurBackgroundColor if isReusing { // Do nothing. } else { if titleLabel.superview == nil { blurView.contentView.addSubview(titleLabel) titleLabel.setContentHuggingLow() } else { NSLayoutConstraint.deactivate(layoutConstraints) } layoutConstraints = titleLabel.autoPinEdgesToSuperviewEdges(with: innerStackConfig.layoutMargins) } } public func reset(resetReusableState: Bool) { if resetReusableState { titleLabel.removeFromSuperview() } titleLabel.text = nil layoutConstraints = [] } } // MARK: - private class ContentViewDefault { private let titleLabel = CVLabel() private let innerStack = ManualStackView(name: "dateHeader.innerStackView") var rootView: UIView { innerStack } fileprivate var wallpaperBlurView: CVWallpaperBlurView? private func ensureWallpaperBlurView() -> CVWallpaperBlurView { if let wallpaperBlurView = self.wallpaperBlurView { return wallpaperBlurView } let wallpaperBlurView = CVWallpaperBlurView() self.wallpaperBlurView = wallpaperBlurView return wallpaperBlurView } func configure(componentView: CVComponentDateHeader.CVComponentViewDateHeader, cellMeasurement: CVCellMeasurement, componentDelegate: CVComponentDelegate, hasWallpaper: Bool, titleLabelConfig: CVLabelConfig, innerStackConfig: CVStackViewConfig, isReusing: Bool) { if !isReusing { reset(resetReusableState: true) } titleLabelConfig.applyForRendering(label: titleLabel) if isReusing { innerStack.configureForReuse(config: innerStackConfig, cellMeasurement: cellMeasurement, measurementKey: CVComponentDateHeader.measurementKey_innerStack) } else { if hasWallpaper { let wallpaperBlurView = ensureWallpaperBlurView() CVComponentBase.configureWallpaperBlurView(wallpaperBlurView: wallpaperBlurView, maskCornerRadius: 8, componentDelegate: componentDelegate) innerStack.addSubviewToFillSuperviewEdges(wallpaperBlurView) } innerStack.configure(config: innerStackConfig, cellMeasurement: cellMeasurement, measurementKey: CVComponentDateHeader.measurementKey_innerStack, subviews: [ titleLabel ]) } } public func reset(resetReusableState: Bool) { if resetReusableState { innerStack.reset() titleLabel.removeFromSuperview() wallpaperBlurView?.removeFromSuperview() wallpaperBlurView?.resetContentAndConfiguration() } titleLabel.text = nil } }