Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/ThreadSettings/GroupDescriptionViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

protocol GroupDescriptionViewControllerDelegate: AnyObject {
    func groupDescriptionViewControllerDidComplete(groupDescription: String?)
}

class GroupDescriptionViewController: OWSTableViewController2 {
    private let helper: GroupAttributesEditorHelper
    private let providedCurrentDescription: Bool

    weak var descriptionDelegate: GroupDescriptionViewControllerDelegate?

    private let options: Options
    struct Options: OptionSet {
        let rawValue: Int

        static let updateImmediately = Options(rawValue: 1 << 0)
        /// The view should present itself already in edit mode
        static let editImmediately = Options(rawValue: 1 << 1)
        /// The view should give the user the option to enter edit mode
        static let canEdit = Options(rawValue: 1 << 2)
    }

    private var isInEditMode: Bool

    convenience init(
        groupModel: TSGroupModel,
        groupDescriptionCurrent: String? = nil,
        options: Options,
    ) {
        self.init(
            helper: GroupAttributesEditorHelper(groupModel: groupModel),
            groupDescriptionCurrent: groupDescriptionCurrent,
            options: options,
        )
    }

    init(
        helper: GroupAttributesEditorHelper,
        groupDescriptionCurrent: String? = nil,
        options: Options = [],
    ) {
        self.helper = helper
        self.options = options
        self.isInEditMode = options.contains(.editImmediately)

        if let groupDescriptionCurrent {
            self.helper.groupDescriptionOriginal = groupDescriptionCurrent
            providedCurrentDescription = true
        } else {
            providedCurrentDescription = false
        }

        super.init()

        shouldAvoidKeyboard = true
    }

    // MARK: -

    override func viewDidLoad() {
        super.viewDidLoad()

        helper.delegate = self
        helper.buildContents()

        updateTableContents()
        helper.descriptionTextView.linkTextAttributes = [
            .foregroundColor: UIColor.Signal.link,
            .underlineStyle: NSUnderlineStyle.single.rawValue,
        ]
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(alongsideTransition: { _ in
            self.helper.descriptionTextView.scrollToFocus(in: self.tableView, animated: true)
        }, completion: nil)
    }

    // Don't allow interactive dismiss when there are unsaved changes.
    override var isModalInPresentation: Bool {
        get { helper.hasUnsavedChanges }
        set {}
    }

    private func updateNavigation() {
        updateNavigationTitle()
        updateNavigationItems()
    }

    private func updateNavigationTitle() {
        let currentGlyphCount = (helper.groupDescriptionCurrent ?? "").glyphCount
        let remainingGlyphCount = max(0, GroupManager.maxGroupDescriptionGlyphCount - currentGlyphCount)

        if !isInEditMode, let groupName = helper.groupNameCurrent {
            if self.providedCurrentDescription {
                // don't assume the current group title applies
                // if a group description was directly provided.
                title = nil
            } else {
                title = groupName
            }
        } else if isInEditMode, remainingGlyphCount <= 100 {
            let titleFormat = OWSLocalizedString(
                "GROUP_DESCRIPTION_VIEW_TITLE_FORMAT",
                comment: "Title for the group description view. Embeds {{ the number of characters that can be added to the description without hitting the length limit }}.",
            )
            title = String.nonPluralLocalizedStringWithFormat(titleFormat, OWSFormat.formatInt(remainingGlyphCount))
        } else {
            title = OWSLocalizedString(
                "GROUP_DESCRIPTION_VIEW_TITLE",
                comment: "Title for the group description view.",
            )
        }
    }

    private func updateNavigationItems() {
        if isInEditMode {
            navigationItem.leftBarButtonItem = .cancelButton(
                dismissingFrom: self,
                hasUnsavedChanges: { [weak self] in self?.helper.hasUnsavedChanges },
            )

            navigationItem.rightBarButtonItem = .setButton { [weak self] in
                guard let self else { return }
                if self.options.contains(.updateImmediately) {
                    self.didTapSet()
                } else {
                    self.didTapDone()
                }
            }

            navigationItem.rightBarButtonItem?.isEnabled = helper.hasUnsavedChanges
        } else {
            let isEditable = options.contains(.canEdit) || options.contains(.editImmediately)
            if isEditable {
                navigationItem.leftBarButtonItem = .systemItem(.edit) { [weak self] in
                    guard let self else { return }
                    self.isInEditMode = true
                    self.updateTableContents()
                    UIView.animate(withDuration: 0.3) {
                        self.updateNavigationItems()
                    } completion: { _ in
                        self.updateNavigationTitle()
                        self.helper.descriptionTextView.becomeFirstResponder()
                    }
                }
            }

            navigationItem.rightBarButtonItem = .doneButton(dismissingFrom: self)
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateNavigation()

        if isInEditMode {
            helper.descriptionTextView.becomeFirstResponder()
        }
    }

    func updateTableContents() {
        let contents = OWSTableContents()

        let descriptionTextView = helper.descriptionTextView

        let section = OWSTableSection()
        descriptionTextView.isEditable = self.isInEditMode
        section.add(self.textViewItem(
            descriptionTextView,
            minimumHeight: self.isInEditMode ? 74 : nil,
            dataDetectorTypes: self.isInEditMode ? [] : .all,
        ))

        if isInEditMode {
            section.footerTitle = OWSLocalizedString(
                "GROUP_DESCRIPTION_VIEW_EDIT_FOOTER",
                comment: "Footer text when editing the group description",
            )
        }

        contents.add(section)

        self.contents = contents
    }

    private func didTapDone() {
        helper.descriptionTextView.acceptAutocorrectSuggestion()
        descriptionDelegate?.groupDescriptionViewControllerDidComplete(groupDescription: helper.groupDescriptionCurrent)
        dismiss(animated: true)
    }

    private func didTapSet() {
        guard isInEditMode, helper.hasUnsavedChanges else {
            return owsFailDebug("Unexpectedly trying to set")
        }

        helper.updateGroupIfNecessary(fromViewController: self) { [weak self] in
            guard let self else { return }
            self.descriptionDelegate?.groupDescriptionViewControllerDidComplete(
                groupDescription: self.helper.groupDescriptionCurrent,
            )
            self.dismiss(animated: true)
        }
    }
}

extension GroupDescriptionViewController: GroupAttributesEditorHelperDelegate, TextViewWithPlaceholderDelegate {
    func groupAttributesEditorContentsDidChange() {
        updateNavigation()
        textViewDidUpdateText(helper.descriptionTextView)
    }

    func groupAttributesEditorSelectionDidChange() {
        textViewDidUpdateSelection(helper.descriptionTextView)
    }
}