Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Calls/UserInterface/Survey/CallQualitySurveyDebugLogViewController.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Combine
import SignalServiceKit
import SignalUI

final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
    private var sizeChangeSubscription: AnyCancellable?

    private let headerContainer = UIView()
    private let bottomStackView = UIStackView()

    private let tableViewController = OWSTableViewController2()

    private var shouldSubmitDebugLogs = false

    private let rating: CallQualitySurvey.Rating

    init(rating: CallQualitySurvey.Rating) {
        self.rating = rating
        super.init(nibName: nil, bundle: nil)
    }

    @MainActor
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString(
            "CALL_QUALITY_SURVEY_DEBUG_LOG_TITLE",
            comment: "Title for the debug log sharing screen in the call quality survey",
        )

        let headerLabel = createHeaderView()
        headerContainer.addSubview(headerLabel)
        headerLabel.autoPinEdgesToSuperviewMargins(with: .init(
            top: 0,
            leading: 36,
            bottom: 0,
            trailing: 36,
        ))
        view.addSubview(headerContainer)
        headerContainer.autoPinEdges(toSuperviewEdgesExcludingEdge: .bottom)
        headerContainer.layoutMargins = .zero
        headerContainer.preservesSuperviewLayoutMargins = true

        tableViewController.backgroundStyle = .none
        addChild(tableViewController)
        view.addSubview(tableViewController.view)
        tableViewController.tableView.alwaysBounceVertical = false
        tableViewController.view.autoPinWidthToSuperview()
        tableViewController.view.autoPinEdge(.top, to: .bottom, of: headerContainer)
        tableViewController.didMove(toParent: self)

        let section = OWSTableSection(
            title: nil,
            items: [
                .switch(
                    withText: OWSLocalizedString(
                        "CALL_QUALITY_SURVEY_DEBUG_LOG_TOGGLE",
                        comment: "Label for the toggle to share debug log in the call quality survey",
                    ),
                    isOn: { [weak self] in
                        self?.shouldSubmitDebugLogs ?? false
                    },
                    actionBlock: { [weak self] control in
                        self?.shouldSubmitDebugLogs = control.isOn
                    },
                ),
            ],
        )
        section.customFooterView = createFooterView()

        tableViewController.setContents(OWSTableContents(sections: [section]))

        bottomStackView.axis = .vertical
        bottomStackView.spacing = 24
        bottomStackView.isLayoutMarginsRelativeArrangement = true
        bottomStackView.directionalLayoutMargins = .init(hMargin: 12, vMargin: 0)
        view.addSubview(bottomStackView)
        bottomStackView.autoPinEdge(.top, to: .bottom, of: tableViewController.view)
        bottomStackView.autoPinEdges(toSuperviewMarginsExcludingEdge: .top)

        let continueButton = UIButton(primaryAction: .init { [weak self] _ in
            self?.submit()
        })
        continueButton.configuration = .largePrimary(title: OWSLocalizedString(
            "CALL_QUALITY_SURVEY_SUBMIT_BUTTON",
            comment: "Button text to submit the call quality survey",
        ))
        bottomStackView.addArrangedSubview(continueButton)

        if #available(iOS 16.0, *) {
            sizeChangeSubscription = tableViewController.tableView
                .publisher(for: \.contentSize)
                .removeDuplicates()
                .sink { [weak self] contentSize in
                    DispatchQueue.main.async {
                        self?.reloadHeight()
                    }
                }
        }
    }

    private func createHeaderView() -> UIView {
        let textView = LinkingTextView { [weak self] in
            self?.showDiagnosticsPreview()
        }
        textView.attributedText = OWSLocalizedString(
            "CALL_QUALITY_SURVEY_DEBUG_LOG_HEADER",
            comment: "Header text explaining the purpose of sharing debug logs in the call quality survey. The text inside the <link> tags is tappable for viewing the diagnostic information.",
        )
        .styled(
            with: .font(.dynamicTypeSubheadline),
            .color(.Signal.secondaryLabel),
            .alignment(.center),
            .xmlRules([.style("link", .init(.link(.Support.generic)))]),
        )
        return textView
    }

    private func createFooterView() -> UIView {
        let container = UIView()

        let textView = LinkingTextView { [weak self] in
            self?.showDebugLogPreview()
        }
        textView.attributedText = .composed(of: [
            OWSLocalizedString(
                "CALL_QUALITY_SURVEY_DEBUG_LOG_FOOTER",
                comment: "Footer text explaining what debug logs contain in the call quality survey",
            ),
            " ",
            OWSLocalizedString(
                "CALL_QUALITY_SURVEY_DEBUG_LOG_PREVIEW_LINK",
                comment: "Link text to open a preview of debug logs in the call quality survey. Appended to CALL_QUALITY_SURVEY_DEBUG_LOG_FOOTER",
            ).styled(with: .link(.Support.generic)),
        ]).styled(
            with: .font(.dynamicTypeFootnote),
            .color(.Signal.secondaryLabel),
        )
        container.addSubview(textView)
        textView.autoPinEdgesToSuperviewEdges(with: .init(top: 12, leading: 20, bottom: 0, trailing: 20))

        return container
    }

    private func showDiagnosticsPreview() {
        let protoJSON = sheetNav?.callQualitySurveyManager.protoJSONPreview(rating: rating)
        guard let protoJSON else { return }

        let vc = OWSTableViewController2()
        let section = OWSTableSection(items: [
            .init(customCellBlock: {
                let cell = UITableViewCell()

                let textView = UITextView()
                textView.backgroundColor = .clear
                textView.isOpaque = false
                textView.isEditable = false
                textView.textContainerInset = .zero
                textView.contentInset = .zero
                textView.textContainer.lineFragmentPadding = 0
                textView.isScrollEnabled = false
                textView.isSelectable = false

                textView.font = .dynamicTypeSubheadline
                textView.textColor = .Signal.secondaryLabel
                textView.text = protoJSON

                cell.contentView.addSubview(textView)
                textView.autoPinEdgesToSuperviewMargins()

                return cell
            }),
        ])
        let contents = OWSTableContents(
            title: OWSLocalizedString(
                "CALL_QUALITY_SURVEY_DIAGNOSTICS_TITLE",
                comment: "Title for preview of the call diagnostic info that will be sent with the survey",
            ),
            sections: [section],
        )
        vc.setContents(contents)
        vc.navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: vc)
        let nav = OWSNavigationController(rootViewController: vc)
        present(nav, animated: true)
    }

    private func showDebugLogPreview() {
        let vc = DebugLogPreviewViewController()
        let nav = OWSNavigationController(rootViewController: vc)
        present(nav, animated: true)
    }

    override func customSheetHeight() -> CGFloat? {
        let headerHeight = headerContainer.height
        let collectionViewHeight = tableViewController.tableView.contentSize.height + tableViewController.tableView.contentInset.totalHeight
        let bottomStackHeight = bottomStackView.height
        return headerHeight + collectionViewHeight + bottomStackHeight
    }

    private func submit() {
        sheetNav?.submit(
            rating: self.rating,
            shouldSubmitDebugLogs: self.shouldSubmitDebugLogs,
        )
    }
}