Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/ConversationView/Components/CVComponentTypingIndicator.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
public import SignalUI

public class CVComponentTypingIndicator: CVComponentBase, CVRootComponent {

    public var componentKey: CVComponentKey { .typingIndicator }

    public var cellReuseIdentifier: CVCellReuseIdentifier {
        CVCellReuseIdentifier.typingIndicator
    }

    public let isDedicatedCell = true

    private let typingIndicator: CVComponentState.TypingIndicator

    init(
        itemModel: CVItemModel,
        typingIndicator: CVComponentState.TypingIndicator,
    ) {
        self.typingIndicator = typingIndicator

        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,
        )
    }

    override public func wallpaperBlurView(componentView: CVComponentView) -> CVWallpaperBlurView? {
        guard let componentView = componentView as? CVComponentViewTypingIndicator else {
            owsFailDebug("Unexpected componentView.")
            return nil
        }
        return componentView.wallpaperBlurView
    }

    public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
        CVComponentViewTypingIndicator()
    }

    public func configureForRendering(
        componentView: CVComponentView,
        cellMeasurement: CVCellMeasurement,
        componentDelegate: CVComponentDelegate,
    ) {
        guard let componentView = componentView as? CVComponentViewTypingIndicator else {
            owsFailDebug("Unexpected componentView.")
            return
        }

        // TODO: Reuse?

        let outerStackView = componentView.outerStackView
        let innerStackView = componentView.innerStackView

        innerStackView.reset()
        outerStackView.reset()

        var outerViews = [UIView]()

        if let avatarDataSource = typingIndicator.avatarDataSource {
            let avatarView = componentView.avatarView
            avatarView.updateWithSneakyTransactionIfNecessary { config in
                config.dataSource = avatarDataSource
            }
            outerViews.append(avatarView)
        }

        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
        }
        innerStackView.addSubviewToFillSuperviewEdges(bubbleView)

        let typingIndicatorView = componentView.typingIndicatorView
        typingIndicatorView.configureForConversationView(cellMeasurement: cellMeasurement)

        outerViews.append(innerStackView)

        // We always use a stretching spacer.
        outerViews.append(UIView.hStretchingSpacer())

        innerStackView.configure(
            config: innerStackViewConfig,
            cellMeasurement: cellMeasurement,
            measurementKey: Self.measurementKey_innerStack,
            subviews: [typingIndicatorView],
        )
        outerStackView.configure(
            config: outerStackViewConfig,
            cellMeasurement: cellMeasurement,
            measurementKey: Self.measurementKey_outerStack,
            subviews: outerViews,
        )
    }

    private var outerStackViewConfig: CVStackViewConfig {
        CVStackViewConfig(
            axis: .horizontal,
            alignment: .center,
            spacing: ConversationStyle.messageStackSpacing,
            layoutMargins: UIEdgeInsets(
                top: 0,
                leading: conversationStyle.gutterLeading,
                bottom: 0,
                trailing: conversationStyle.gutterTrailing,
            ),
        )
    }

    private var innerStackViewConfig: CVStackViewConfig {
        CVStackViewConfig(
            axis: .horizontal,
            alignment: .center,
            spacing: 0,
            layoutMargins: conversationStyle.textInsets,
        )
    }

    private let minBubbleHeight: CGFloat = 36

    private static let measurementKey_outerStack = "CVComponentTypingIndicator.measurementKey_outerStack"
    private static let measurementKey_innerStack = "CVComponentTypingIndicator.measurementKey_innerStack"

    public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
        owsAssertDebug(maxWidth > 0)

        var outerSubviewInfos = [ManualStackSubviewInfo]()
        var innerSubviewInfos = [ManualStackSubviewInfo]()

        if typingIndicator.avatarDataSource != nil {
            let avatarSize: CGSize = ConversationStyle.groupMessageAvatarSizeClass.size
            outerSubviewInfos.append(avatarSize.asManualSubviewInfo(hasFixedSize: true))
        }

        let typingIndicatorSize = TypingIndicatorView.measure(measurementBuilder: measurementBuilder)
        innerSubviewInfos.append(typingIndicatorSize.asManualSubviewInfo(hasFixedSize: true))

        let innerStackMeasurement = ManualStackView.measure(
            config: innerStackViewConfig,
            measurementBuilder: measurementBuilder,
            measurementKey: Self.measurementKey_innerStack,
            subviewInfos: innerSubviewInfos,
        )
        var innerStackSize = innerStackMeasurement.measuredSize
        innerStackSize.height = max(minBubbleHeight, innerStackSize.height)
        outerSubviewInfos.append(innerStackSize.asManualSubviewInfo(hasFixedWidth: true))

        // We always use a stretching spacer.
        outerSubviewInfos.append(ManualStackSubviewInfo.empty)

        let outerStackMeasurement = ManualStackView.measure(
            config: outerStackViewConfig,
            measurementBuilder: measurementBuilder,
            measurementKey: Self.measurementKey_outerStack,
            subviewInfos: outerSubviewInfos,
            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 CVComponentViewTypingIndicator: NSObject, CVComponentView {

        fileprivate let outerStackView = ManualStackView(name: "Typing indicator outer")
        fileprivate let innerStackView = ManualStackView(name: "Typing indicator inner")

        fileprivate let avatarView = ConversationAvatarView(
            sizeClass: ConversationStyle.groupMessageAvatarSizeClass,
            localUserDisplayMode: .asUser,
            useAutolayout: false,
        )
        // 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
        }

        fileprivate let typingIndicatorView = TypingIndicatorView()

        public var isDedicatedCellView = false

        public var rootView: UIView {
            outerStackView
        }

        // MARK: -

        public func setIsCellVisible(_ isCellVisible: Bool) {
            if isCellVisible {
                typingIndicatorView.startAnimation()
            } else {
                typingIndicatorView.stopAnimation()
            }
        }

        public func reset() {
            owsAssertDebug(isDedicatedCellView)

            outerStackView.reset()
            innerStackView.reset()
            avatarView.reset()

            chatColorView.reset()
            chatColorView.removeFromSuperview()

            wallpaperBlurView?.removeFromSuperview()

            typingIndicatorView.reset()
            typingIndicatorView.removeFromSuperview()
        }
    }
}