391 lines
16 KiB
Swift
391 lines
16 KiB
Swift
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
// TODO: We should describe which state updates & when it is committed.
|
|
extension ConversationSettingsViewController {
|
|
|
|
private var subtitlePointSize: CGFloat {
|
|
return 12
|
|
}
|
|
|
|
private var threadName: String {
|
|
var threadName = contactsManager.displayNameWithSneakyTransaction(thread: thread)
|
|
|
|
if let contactThread = thread as? TSContactThread {
|
|
if let phoneNumber = contactThread.contactAddress.phoneNumber,
|
|
phoneNumber == threadName {
|
|
threadName = PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: phoneNumber)
|
|
}
|
|
}
|
|
|
|
return threadName
|
|
}
|
|
|
|
private struct HeaderBuilder {
|
|
let viewController: ConversationSettingsViewController
|
|
|
|
var subviews = [UIView]()
|
|
|
|
init(viewController: ConversationSettingsViewController) {
|
|
self.viewController = viewController
|
|
|
|
addFirstSubviews()
|
|
}
|
|
|
|
mutating func addFirstSubviews() {
|
|
let avatarView = buildAvatarView()
|
|
|
|
let avatarWrapper = UIView.container()
|
|
avatarWrapper.addSubview(avatarView)
|
|
avatarView.autoPinEdgesToSuperviewEdges()
|
|
|
|
if let groupThread = viewController.thread as? TSGroupThread,
|
|
groupThread.groupModel.groupAvatarData == nil,
|
|
viewController.canEditConversationAttributes {
|
|
let cameraButton = GroupAttributesEditorHelper.buildCameraButtonForCorner()
|
|
avatarWrapper.addSubview(cameraButton)
|
|
cameraButton.autoPinEdge(toSuperviewEdge: .trailing)
|
|
cameraButton.autoPinEdge(toSuperviewEdge: .bottom)
|
|
}
|
|
|
|
subviews.append(avatarWrapper)
|
|
subviews.append(UIView.spacer(withHeight: 8))
|
|
subviews.append(buildThreadNameLabel())
|
|
}
|
|
|
|
func buildAvatarView() -> UIView {
|
|
let avatarSize: UInt = kLargeAvatarSize
|
|
let avatarImage = OWSAvatarBuilder.buildImage(thread: viewController.thread,
|
|
diameter: avatarSize)
|
|
let avatarView = AvatarImageView(image: avatarImage)
|
|
avatarView.autoSetDimensions(to: CGSize(square: CGFloat(avatarSize)))
|
|
// Track the most recent avatar view.
|
|
viewController.avatarView = avatarView
|
|
return avatarView
|
|
}
|
|
|
|
func buildThreadNameLabel() -> UILabel {
|
|
let label = UILabel()
|
|
label.text = viewController.threadName
|
|
label.textColor = Theme.primaryTextColor
|
|
label.font = UIFont.ows_dynamicTypeTitle2.ows_semibold()
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}
|
|
|
|
mutating func addSubtitleLabel(text: String, font: UIFont? = nil) {
|
|
addSubtitleLabel(attributedText: NSAttributedString(string: text), font: font)
|
|
}
|
|
|
|
mutating func addSubtitleLabel(attributedText: NSAttributedString, font: UIFont? = nil) {
|
|
subviews.append(UIView.spacer(withHeight: 2))
|
|
subviews.append(buildHeaderSubtitleLabel(attributedText: attributedText, font: font))
|
|
}
|
|
|
|
mutating func addLegacyGroupView() -> UIView {
|
|
subviews.append(UIView.spacer(withHeight: 12))
|
|
|
|
let bubbleView = UIView()
|
|
bubbleView.backgroundColor = Theme.secondaryBackgroundColor
|
|
bubbleView.layer.cornerRadius = 4
|
|
bubbleView.layoutMargins = UIEdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12)
|
|
subviews.append(bubbleView)
|
|
|
|
let label = UILabel()
|
|
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_FORMAT",
|
|
comment: "Brief explanation of legacy groups. Embeds {{ a \"learn more\" link. }}.")
|
|
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
|
|
comment: "A \"learn more\" link with more information about legacy groups.")
|
|
let text = String(format: format, learnMoreText)
|
|
let attributedString = NSMutableAttributedString(string: text)
|
|
attributedString.setAttributes([
|
|
.foregroundColor: Theme.accentBlueColor
|
|
],
|
|
forSubstring: learnMoreText)
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
label.font = .ows_dynamicTypeFootnote
|
|
label.attributedText = attributedString
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
label.setCompressionResistanceHigh()
|
|
bubbleView.addSubview(label)
|
|
label.autoPinEdgesToSuperviewMargins()
|
|
|
|
return bubbleView
|
|
}
|
|
|
|
func buildHeaderSubtitleLabel(attributedText: NSAttributedString,
|
|
font: UIFont?) -> UILabel {
|
|
let label = UILabel()
|
|
|
|
// Defaults need to be set *before* assigning the attributed text,
|
|
// or the attributes will get overriden
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
label.lineBreakMode = .byTruncatingTail
|
|
if let font = font {
|
|
label.font = font
|
|
} else {
|
|
label.font = UIFont.ows_regularFont(withSize: viewController.subtitlePointSize)
|
|
}
|
|
|
|
label.attributedText = attributedText
|
|
|
|
return label
|
|
}
|
|
|
|
mutating func addLastSubviews() {
|
|
// TODO Message Request: In order to debug the profile is getting shared in the right moments,
|
|
// display the thread whitelist state in settings. Eventually we can probably delete this.
|
|
#if DEBUG
|
|
let viewController = self.viewController
|
|
let isThreadInProfileWhitelist =
|
|
viewController.databaseStorage.uiRead { transaction in
|
|
return viewController.profileManager.isThread(inProfileWhitelist: viewController.thread,
|
|
transaction: transaction)
|
|
}
|
|
let hasSharedProfile = String(format: "Whitelisted: %@", isThreadInProfileWhitelist ? "Yes" : "No")
|
|
addSubtitleLabel(text: hasSharedProfile)
|
|
#endif
|
|
}
|
|
|
|
func build() -> UIView {
|
|
let header = UIStackView(arrangedSubviews: subviews)
|
|
header.axis = .vertical
|
|
header.alignment = .center
|
|
header.layoutMargins = UIEdgeInsets(top: 8, leading: 18, bottom: 16, trailing: 18)
|
|
header.isLayoutMarginsRelativeArrangement = true
|
|
|
|
if viewController.canEditConversationAttributes {
|
|
header.addGestureRecognizer(UITapGestureRecognizer(target: viewController, action: #selector(conversationNameTouched)))
|
|
}
|
|
header.isUserInteractionEnabled = true
|
|
header.accessibilityIdentifier = UIView.accessibilityIdentifier(in: viewController, name: "mainSectionHeader")
|
|
header.addBackgroundView(withBackgroundColor: ConversationSettingsViewController.headerBackgroundColor)
|
|
|
|
return header
|
|
}
|
|
}
|
|
|
|
private func buildHeaderForGroup(groupThread: TSGroupThread) -> UIView {
|
|
var builder = HeaderBuilder(viewController: self)
|
|
|
|
let memberCount = groupThread.groupModel.groupMembership.nonPendingMembers.count
|
|
var groupMembersText = GroupViewUtils.formatGroupMembersLabel(memberCount: memberCount)
|
|
if RemoteConfig.groupsV2GoodCitizen,
|
|
groupThread.isGroupV1Thread {
|
|
groupMembersText.append(" ")
|
|
groupMembersText.append("•")
|
|
groupMembersText.append(" ")
|
|
groupMembersText.append(NSLocalizedString("GROUPS_LEGACY_GROUP_INDICATOR",
|
|
comment: "Label indicating a legacy group."))
|
|
}
|
|
builder.addSubtitleLabel(text: groupMembersText,
|
|
font: .ows_dynamicTypeSubheadline)
|
|
|
|
if RemoteConfig.groupsV2GoodCitizen,
|
|
groupThread.isGroupV1Thread {
|
|
let legacyGroupView = builder.addLegacyGroupView()
|
|
legacyGroupView.isUserInteractionEnabled = true
|
|
legacyGroupView.addGestureRecognizer(UITapGestureRecognizer(target: self,
|
|
action: #selector(didTapLegacyGroupView)))
|
|
}
|
|
|
|
builder.addLastSubviews()
|
|
|
|
let header = builder.build()
|
|
|
|
// This will not appear in public builds.
|
|
if DebugFlags.groupsV2showV2Indicator,
|
|
thread.isGroupV2Thread {
|
|
let indicatorLabel = UILabel()
|
|
indicatorLabel.text = thread.isGroupV2Thread ? "v2" : "v1"
|
|
indicatorLabel.textColor = Theme.secondaryTextAndIconColor
|
|
indicatorLabel.font = .ows_dynamicTypeBody
|
|
header.addSubview(indicatorLabel)
|
|
indicatorLabel.autoPinEdge(toSuperviewMargin: .trailing)
|
|
indicatorLabel.autoPinEdge(toSuperviewMargin: .bottom)
|
|
}
|
|
|
|
return header
|
|
}
|
|
|
|
private func buildHeaderForContact(contactThread: TSContactThread) -> UIView {
|
|
var builder = HeaderBuilder(viewController: self)
|
|
|
|
let threadName = contactsManager.displayNameWithSneakyTransaction(thread: contactThread)
|
|
let recipientAddress = contactThread.contactAddress
|
|
if let phoneNumber = recipientAddress.phoneNumber {
|
|
let formattedPhoneNumber =
|
|
PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: phoneNumber)
|
|
if threadName != formattedPhoneNumber {
|
|
builder.addSubtitleLabel(text: formattedPhoneNumber)
|
|
}
|
|
}
|
|
|
|
if let username = (databaseStorage.uiRead { transaction in
|
|
return self.profileManager.username(for: recipientAddress, transaction: transaction)
|
|
}),
|
|
username.count > 0 {
|
|
if let formattedUsername = CommonFormats.formatUsername(username),
|
|
threadName != formattedUsername {
|
|
builder.addSubtitleLabel(text: formattedUsername)
|
|
}
|
|
}
|
|
|
|
if DebugFlags.showProfileKeyAndUuidsIndicator {
|
|
let uuidText = String(format: "UUID: %@", contactThread.contactAddress.uuid?.uuidString ?? "Unknown")
|
|
builder.addSubtitleLabel(text: uuidText)
|
|
}
|
|
|
|
let isVerified = identityManager.verificationState(for: recipientAddress) == .verified
|
|
if isVerified {
|
|
let subtitle = NSMutableAttributedString()
|
|
// "checkmark"
|
|
subtitle.append("\u{f00c} ",
|
|
attributes: [
|
|
.font: UIFont.ows_fontAwesomeFont(subtitlePointSize)
|
|
])
|
|
subtitle.append(NSLocalizedString("PRIVACY_IDENTITY_IS_VERIFIED_BADGE",
|
|
comment: "Badge indicating that the user is verified."))
|
|
builder.addSubtitleLabel(attributedText: subtitle)
|
|
}
|
|
|
|
// This will not appear in public builds.
|
|
if DebugFlags.showProfileKeyAndUuidsIndicator {
|
|
let hasProfileKey = self.databaseStorage.uiRead { transaction in
|
|
self.profileManager.profileKeyData(for: recipientAddress, transaction: transaction) != nil
|
|
}
|
|
|
|
let subtitle = "Has Profile Key: \(hasProfileKey)"
|
|
builder.addSubtitleLabel(attributedText: subtitle.asAttributedString)
|
|
}
|
|
|
|
builder.addLastSubviews()
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
func buildMainHeader() -> UIView {
|
|
if let groupThread = thread as? TSGroupThread {
|
|
return buildHeaderForGroup(groupThread: groupThread)
|
|
} else if let contactThread = thread as? TSContactThread {
|
|
return buildHeaderForContact(contactThread: contactThread)
|
|
} else {
|
|
owsFailDebug("Invalid thread.")
|
|
return UIView()
|
|
}
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
@objc
|
|
func didTapLegacyGroupView(sender: UIGestureRecognizer) {
|
|
LegacyGroupView().present(fromViewController: self)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
class LegacyGroupView: UIView {
|
|
|
|
weak var actionSheetController: ActionSheetController?
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func present(fromViewController: UIViewController) {
|
|
let buildLabel = { () -> UILabel in
|
|
let label = UILabel()
|
|
label.textColor = Theme.primaryTextColor
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
return label
|
|
}
|
|
|
|
let titleLabel = buildLabel()
|
|
titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_semibold()
|
|
titleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_TITLE",
|
|
comment: "Title for the 'legacy group' alert view.")
|
|
|
|
let section1TitleLabel = buildLabel()
|
|
section1TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold()
|
|
section1TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_TITLE",
|
|
comment: "Title for the first section of the 'legacy group' alert view.")
|
|
|
|
let section1BodyLabel = buildLabel()
|
|
section1BodyLabel.font = .ows_dynamicTypeBody
|
|
section1BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_BODY",
|
|
comment: "Body text for the first section of the 'legacy group' alert view.")
|
|
|
|
let section2TitleLabel = buildLabel()
|
|
section2TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold()
|
|
section2TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_TITLE",
|
|
comment: "Title for the second section of the 'legacy group' alert view.")
|
|
|
|
let section2BodyLabel = buildLabel()
|
|
section2BodyLabel.font = .ows_dynamicTypeBody
|
|
section2BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_BODY",
|
|
comment: "Body text for the second section of the 'legacy group' alert view.")
|
|
|
|
let section3BodyLabel = buildLabel()
|
|
section3BodyLabel.font = .ows_dynamicTypeBody
|
|
section3BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_3_BODY",
|
|
comment: "Body text for the third section of the 'legacy group' alert view.")
|
|
|
|
let buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_semibold()
|
|
let buttonHeight = OWSFlatButton.heightForFont(buttonFont)
|
|
let okayButton = OWSFlatButton.button(title: CommonStrings.okayButton,
|
|
font: buttonFont,
|
|
titleColor: .white,
|
|
backgroundColor: .ows_accentBlue,
|
|
target: self,
|
|
selector: #selector(dismissAlert))
|
|
okayButton.autoSetDimension(.height, toSize: buttonHeight)
|
|
|
|
let stackView = UIStackView(arrangedSubviews: [
|
|
titleLabel,
|
|
UIView.spacer(withHeight: 28),
|
|
section1TitleLabel,
|
|
UIView.spacer(withHeight: 4),
|
|
section1BodyLabel,
|
|
UIView.spacer(withHeight: 21),
|
|
section2TitleLabel,
|
|
UIView.spacer(withHeight: 4),
|
|
section2BodyLabel,
|
|
UIView.spacer(withHeight: 24),
|
|
section3BodyLabel,
|
|
UIView.spacer(withHeight: 28),
|
|
okayButton
|
|
])
|
|
stackView.axis = .vertical
|
|
stackView.alignment = .fill
|
|
stackView.layoutMargins = UIEdgeInsets(top: 48, leading: 20, bottom: 38, trailing: 24)
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
stackView.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
|
|
|
|
layoutMargins = .zero
|
|
addSubview(stackView)
|
|
stackView.autoPinEdgesToSuperviewMargins()
|
|
|
|
let actionSheetController = ActionSheetController()
|
|
actionSheetController.customHeader = self
|
|
actionSheetController.isCancelable = true
|
|
fromViewController.presentActionSheet(actionSheetController)
|
|
self.actionSheetController = actionSheetController
|
|
}
|
|
|
|
@objc
|
|
func dismissAlert() {
|
|
actionSheetController?.dismiss(animated: true)
|
|
}
|
|
}
|