698 lines
26 KiB
Swift
698 lines
26 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
struct ConversationHeaderBuilder: Dependencies {
|
|
weak var delegate: ConversationHeaderDelegate!
|
|
let transaction: SDSAnyReadTransaction
|
|
let avatarSize: UInt
|
|
let options: Options
|
|
|
|
var subviews = [UIView]()
|
|
|
|
struct Options: OptionSet {
|
|
let rawValue: Int
|
|
|
|
static let message = Options(rawValue: 1 << 0)
|
|
static let audioCall = Options(rawValue: 1 << 1)
|
|
static let videoCall = Options(rawValue: 1 << 2)
|
|
static let mute = Options(rawValue: 1 << 3)
|
|
static let search = Options(rawValue: 1 << 4)
|
|
|
|
static let renderLocalUserAsNoteToSelf = Options(rawValue: 1 << 5)
|
|
}
|
|
|
|
static func buildHeader(
|
|
for thread: TSThread,
|
|
avatarSize: UInt,
|
|
options: Options,
|
|
delegate: ConversationHeaderDelegate
|
|
) -> UIView {
|
|
if let groupThread = thread as? TSGroupThread {
|
|
return ConversationHeaderBuilder.buildHeaderForGroup(
|
|
groupThread: groupThread,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
delegate: delegate
|
|
)
|
|
} else if let contactThread = thread as? TSContactThread {
|
|
return ConversationHeaderBuilder.buildHeaderForContact(
|
|
contactThread: contactThread,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
delegate: delegate
|
|
)
|
|
} else {
|
|
owsFailDebug("Invalid thread.")
|
|
return UIView()
|
|
}
|
|
}
|
|
|
|
static func buildHeaderForGroup(
|
|
groupThread: TSGroupThread,
|
|
avatarSize: UInt,
|
|
options: Options,
|
|
delegate: ConversationHeaderDelegate
|
|
) -> UIView {
|
|
// Make sure the view is loaded before we open a transaction,
|
|
// because it can end up creating a transaction within.
|
|
_ = delegate.view
|
|
return databaseStorage.read { transaction in
|
|
self.buildHeaderForGroup(
|
|
groupThread: groupThread,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
delegate: delegate,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
|
|
static func buildHeaderForGroup(
|
|
groupThread: TSGroupThread,
|
|
avatarSize: UInt,
|
|
options: Options,
|
|
delegate: ConversationHeaderDelegate,
|
|
transaction: SDSAnyReadTransaction
|
|
) -> UIView {
|
|
var builder = ConversationHeaderBuilder(
|
|
delegate: delegate,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
transaction: transaction
|
|
)
|
|
|
|
var isShowingGroupDescription = false
|
|
if let groupModel = groupThread.groupModel as? TSGroupModelV2 {
|
|
if let descriptionText = groupModel.descriptionText {
|
|
isShowingGroupDescription = true
|
|
builder.addGroupDescriptionPreview(text: descriptionText)
|
|
} else if delegate.canEditConversationAttributes {
|
|
isShowingGroupDescription = true
|
|
builder.addCreateGroupDescriptionButton()
|
|
}
|
|
}
|
|
|
|
if !isShowingGroupDescription && !groupThread.groupModel.isPlaceholder {
|
|
let memberCount = groupThread.groupModel.groupMembership.fullMembers.count
|
|
var groupMembersText = GroupViewUtils.formatGroupMembersLabel(memberCount: memberCount)
|
|
if 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)
|
|
}
|
|
|
|
if groupThread.isGroupV1Thread {
|
|
builder.addLegacyGroupView(groupThread: groupThread)
|
|
}
|
|
|
|
builder.addButtons()
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
static func buildHeaderForContact(
|
|
contactThread: TSContactThread,
|
|
avatarSize: UInt,
|
|
options: Options,
|
|
delegate: ConversationHeaderDelegate
|
|
) -> UIView {
|
|
// Make sure the view is loaded before we open a transaction,
|
|
// because it can end up creating a transaction within.
|
|
_ = delegate.view
|
|
return databaseStorage.read { transaction in
|
|
self.buildHeaderForContact(
|
|
contactThread: contactThread,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
delegate: delegate,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
|
|
static func buildHeaderForContact(
|
|
contactThread: TSContactThread,
|
|
avatarSize: UInt,
|
|
options: Options,
|
|
delegate: ConversationHeaderDelegate,
|
|
transaction: SDSAnyReadTransaction
|
|
) -> UIView {
|
|
var builder = ConversationHeaderBuilder(
|
|
delegate: delegate,
|
|
avatarSize: avatarSize,
|
|
options: options,
|
|
transaction: transaction
|
|
)
|
|
|
|
if !contactThread.contactAddress.isLocalAddress,
|
|
let bioText = profileManagerImpl.profileBioForDisplay(
|
|
for: contactThread.contactAddress,
|
|
transaction: transaction
|
|
) {
|
|
let label = builder.addSubtitleLabel(text: bioText)
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
label.textAlignment = .center
|
|
}
|
|
|
|
let threadName = delegate.threadName(
|
|
renderLocalUserAsNoteToSelf: options.contains(.renderLocalUserAsNoteToSelf),
|
|
transaction: transaction
|
|
)
|
|
let recipientAddress = contactThread.contactAddress
|
|
if let phoneNumber = recipientAddress.phoneNumber {
|
|
let formattedPhoneNumber =
|
|
PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: phoneNumber)
|
|
if threadName != formattedPhoneNumber {
|
|
func copyContactPhoneNumber(delegate: ConversationHeaderDelegate?) {
|
|
guard let delegate = delegate else {
|
|
owsFailDebug("Missing delegate.")
|
|
return
|
|
}
|
|
UIPasteboard.general.string = recipientAddress.phoneNumber
|
|
|
|
let toast = NSLocalizedString("COPIED_TO_CLIPBOARD",
|
|
comment: "Indicator that a value has been copied to the clipboard.")
|
|
delegate.tableViewController.presentToast(text: toast)
|
|
}
|
|
let label = builder.addSubtitleLabel(text: formattedPhoneNumber)
|
|
label.addTapGesture { [weak delegate] in
|
|
copyContactPhoneNumber(delegate: delegate)
|
|
}
|
|
label.addLongPressGesture { [weak delegate] in
|
|
copyContactPhoneNumber(delegate: delegate)
|
|
}
|
|
}
|
|
}
|
|
|
|
let isVerified = identityManager.verificationState(
|
|
for: recipientAddress,
|
|
transaction: transaction
|
|
) == .verified
|
|
if isVerified {
|
|
let subtitle = NSMutableAttributedString()
|
|
subtitle.appendTemplatedImage(named: "check-12", font: .ows_dynamicTypeSubheadlineClamped)
|
|
subtitle.append(" ")
|
|
subtitle.append(NSLocalizedString("PRIVACY_IDENTITY_IS_VERIFIED_BADGE",
|
|
comment: "Badge indicating that the user is verified."))
|
|
builder.addSubtitleLabel(attributedText: subtitle)
|
|
}
|
|
|
|
builder.addButtons()
|
|
|
|
return builder.build()
|
|
}
|
|
|
|
init(delegate: ConversationHeaderDelegate, avatarSize: UInt, options: Options, transaction: SDSAnyReadTransaction) {
|
|
|
|
self.delegate = delegate
|
|
self.avatarSize = avatarSize
|
|
self.options = options
|
|
self.transaction = transaction
|
|
|
|
addFirstSubviews(transaction: transaction)
|
|
}
|
|
|
|
mutating func addFirstSubviews(transaction: SDSAnyReadTransaction) {
|
|
let avatarView = buildAvatarView(transaction: transaction)
|
|
|
|
let avatarWrapper = UIView.container()
|
|
avatarWrapper.addSubview(avatarView)
|
|
avatarView.autoPinEdgesToSuperviewEdges()
|
|
|
|
let avatarButton = OWSButton { [weak delegate] in
|
|
delegate?.tappedAvatar()
|
|
}
|
|
avatarWrapper.addSubview(avatarButton)
|
|
avatarButton.autoPinEdgesToSuperviewEdges()
|
|
|
|
subviews.append(avatarWrapper)
|
|
subviews.append(UIView.spacer(withHeight: 8))
|
|
subviews.append(buildThreadNameLabel())
|
|
}
|
|
|
|
mutating func addButtons() {
|
|
var buttons = [UIView]()
|
|
|
|
if options.contains(.message) {
|
|
buttons.append(buildIconButton(
|
|
icon: .settingsChats,
|
|
text: NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_MESSAGE_BUTTON",
|
|
comment: "Button to message the chat"
|
|
),
|
|
action: { [weak delegate] in
|
|
guard let delegate = delegate else { return }
|
|
delegate.signalApp.presentConversation(for: delegate.thread, action: .compose, animated: true)
|
|
}
|
|
))
|
|
}
|
|
|
|
if ConversationViewController.canCall(threadViewModel: delegate.threadViewModel) {
|
|
let isCurrentCallForThread = callService.currentCall?.thread.uniqueId == delegate.thread.uniqueId
|
|
let hasCurrentCall = callService.currentCall != nil
|
|
|
|
if options.contains(.videoCall) {
|
|
buttons.append(buildIconButton(
|
|
icon: .videoCall,
|
|
text: NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_VIDEO_CALL_BUTTON",
|
|
comment: "Button to start a video call"
|
|
),
|
|
isEnabled: isCurrentCallForThread || !hasCurrentCall,
|
|
action: { [weak delegate] in
|
|
delegate?.startCall(withVideo: true)
|
|
}
|
|
))
|
|
}
|
|
|
|
if !delegate.thread.isGroupThread, options.contains(.audioCall) {
|
|
buttons.append(buildIconButton(
|
|
icon: .audioCall,
|
|
text: NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_AUDIO_CALL_BUTTON",
|
|
comment: "Button to start a audio call"
|
|
),
|
|
isEnabled: isCurrentCallForThread || !hasCurrentCall,
|
|
action: { [weak delegate] in
|
|
delegate?.startCall(withVideo: false)
|
|
}
|
|
))
|
|
}
|
|
}
|
|
|
|
if options.contains(.mute) {
|
|
buttons.append(buildIconButton(
|
|
icon: .settingsMuted,
|
|
text: delegate.threadViewModel.isMuted
|
|
? NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_MUTED_BUTTON",
|
|
comment: "Button to unmute the chat"
|
|
)
|
|
: NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_MUTE_BUTTON",
|
|
comment: "Button to mute the chat"
|
|
),
|
|
action: { [weak delegate] in
|
|
guard let delegate = delegate else { return }
|
|
ConversationSettingsViewController.showMuteUnmuteActionSheet(
|
|
for: delegate.threadViewModel,
|
|
from: delegate
|
|
) { [weak delegate] in
|
|
delegate?.updateTableContents(shouldReload: true)
|
|
}
|
|
}
|
|
))
|
|
}
|
|
|
|
if options.contains(.search), !delegate.isBlockedByMigration {
|
|
buttons.append(buildIconButton(
|
|
icon: .settingsSearch,
|
|
text: NSLocalizedString(
|
|
"CONVERSATION_SETTINGS_SEARCH_BUTTON",
|
|
comment: "Button to search the chat"
|
|
),
|
|
action: { [weak delegate] in
|
|
delegate?.tappedConversationSearch()
|
|
}
|
|
))
|
|
}
|
|
|
|
let spacerWidth: CGFloat = 8
|
|
let totalSpacerWidth = CGFloat(buttons.count - 1) * spacerWidth
|
|
let maxAvailableButtonWidth = delegate.tableViewController.view.width
|
|
- (delegate.tableViewController.cellOuterInsets.totalWidth + totalSpacerWidth)
|
|
let minButtonWidth = maxAvailableButtonWidth / 4
|
|
|
|
var buttonWidth = max(maxIconButtonWidth, minButtonWidth)
|
|
let needsTwoRows = buttonWidth * CGFloat(buttons.count) > maxAvailableButtonWidth
|
|
if needsTwoRows { buttonWidth = buttonWidth * 2 }
|
|
buttons.forEach { $0.autoSetDimension(.width, toSize: buttonWidth) }
|
|
|
|
func addButtonRow(_ buttons: [UIView]) {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.distribution = .fillEqually
|
|
stackView.spacing = spacerWidth
|
|
buttons.forEach { stackView.addArrangedSubview($0) }
|
|
subviews.append(stackView)
|
|
}
|
|
|
|
subviews.append(.spacer(withHeight: 24))
|
|
|
|
if needsTwoRows {
|
|
addButtonRow(Array(buttons.prefix(Int(ceil(CGFloat(buttons.count) / 2)))))
|
|
subviews.append(.spacer(withHeight: 8))
|
|
addButtonRow(buttons.suffix(Int(floor(CGFloat(buttons.count) / 2))))
|
|
} else {
|
|
addButtonRow(buttons)
|
|
}
|
|
}
|
|
|
|
private var maxIconButtonWidth: CGFloat = 0
|
|
mutating func buildIconButton(icon: ThemeIcon, text: String, isEnabled: Bool = true, action: @escaping () -> Void) -> UIView {
|
|
let button = OWSButton { [weak delegate] in
|
|
delegate?.tappedButton()
|
|
action()
|
|
}
|
|
button.dimsWhenHighlighted = true
|
|
button.isEnabled = isEnabled
|
|
button.layer.cornerRadius = 10
|
|
button.clipsToBounds = true
|
|
button.setBackgroundImage(UIImage(color: delegate.tableViewController.cellBackgroundColor), for: .normal)
|
|
button.accessibilityLabel = text
|
|
|
|
let imageView = UIImageView()
|
|
imageView.setTemplateImageName(Theme.iconName(icon), tintColor: Theme.primaryTextColor)
|
|
imageView.autoSetDimension(.height, toSize: 24)
|
|
imageView.contentMode = .scaleAspectFit
|
|
|
|
button.addSubview(imageView)
|
|
imageView.autoPinWidthToSuperview()
|
|
imageView.autoPinEdge(toSuperviewEdge: .top, withInset: 8)
|
|
|
|
let label = UILabel()
|
|
label.font = .ows_dynamicTypeCaption2Clamped
|
|
label.textColor = Theme.primaryTextColor
|
|
label.textAlignment = .center
|
|
label.text = text
|
|
label.sizeToFit()
|
|
label.setCompressionResistanceHorizontalHigh()
|
|
|
|
let buttonMinimumWidth = label.width + 24
|
|
if maxIconButtonWidth < buttonMinimumWidth {
|
|
maxIconButtonWidth = buttonMinimumWidth
|
|
}
|
|
|
|
button.addSubview(label)
|
|
label.autoPinWidthToSuperview(withMargin: 12)
|
|
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 6)
|
|
label.autoPinEdge(.top, to: .bottom, of: imageView, withOffset: 2)
|
|
|
|
return button
|
|
}
|
|
|
|
mutating func addGroupDescriptionPreview(text: String) {
|
|
let previewView = GroupDescriptionPreviewView()
|
|
previewView.descriptionText = text
|
|
previewView.groupName = delegate.threadName(
|
|
renderLocalUserAsNoteToSelf: true,
|
|
transaction: transaction
|
|
)
|
|
previewView.font = .ows_dynamicTypeSubheadlineClamped
|
|
previewView.textColor = Theme.secondaryTextAndIconColor
|
|
previewView.textAlignment = .center
|
|
previewView.numberOfLines = 2
|
|
|
|
subviews.append(UIView.spacer(withHeight: hasSubtitleLabel ? 4 : 8))
|
|
subviews.append(previewView)
|
|
hasSubtitleLabel = true
|
|
}
|
|
|
|
mutating func addCreateGroupDescriptionButton() {
|
|
let button = OWSButton { [weak delegate] in delegate?.didTapAddGroupDescription() }
|
|
button.setTitle(NSLocalizedString(
|
|
"GROUP_DESCRIPTION_PLACEHOLDER",
|
|
comment: "Placeholder text for 'group description' field."
|
|
), for: .normal)
|
|
button.setTitleColor(Theme.secondaryTextAndIconColor, for: .normal)
|
|
button.titleLabel?.font = .ows_dynamicTypeSubheadlineClamped
|
|
|
|
subviews.append(UIView.spacer(withHeight: hasSubtitleLabel ? 4 : 8))
|
|
subviews.append(button)
|
|
hasSubtitleLabel = true
|
|
}
|
|
|
|
func buildAvatarView(transaction: SDSAnyReadTransaction) -> UIView {
|
|
let localUserDisplayMode: LocalUserDisplayMode = (options.contains(.renderLocalUserAsNoteToSelf)
|
|
? .noteToSelf
|
|
: .asUser)
|
|
let avatarView = ConversationAvatarView(diameterPoints: avatarSize,
|
|
localUserDisplayMode: localUserDisplayMode)
|
|
avatarView.configure(thread: delegate.thread, transaction: transaction)
|
|
// Track the most recent avatar view.
|
|
delegate.avatarView = avatarView
|
|
return avatarView
|
|
}
|
|
|
|
func buildThreadNameLabel() -> UILabel {
|
|
let label = UILabel()
|
|
label.text = delegate.threadName(
|
|
renderLocalUserAsNoteToSelf: options.contains(.renderLocalUserAsNoteToSelf),
|
|
transaction: transaction
|
|
)
|
|
label.textColor = Theme.primaryTextColor
|
|
// TODO: See if design really wants this custom font size.
|
|
label.font = UIFont.ows_semiboldFont(withSize: UIFont.ows_dynamicTypeTitle1Clamped.pointSize * (13/14))
|
|
label.lineBreakMode = .byTruncatingTail
|
|
return label
|
|
}
|
|
|
|
@discardableResult
|
|
mutating func addSubtitleLabel(text: String) -> OWSLabel {
|
|
addSubtitleLabel(attributedText: NSAttributedString(string: text))
|
|
}
|
|
|
|
private var hasSubtitleLabel = false
|
|
|
|
@discardableResult
|
|
mutating func addSubtitleLabel(attributedText: NSAttributedString) -> OWSLabel {
|
|
subviews.append(UIView.spacer(withHeight: hasSubtitleLabel ? 4 : 8))
|
|
let label = buildHeaderSubtitleLabel(attributedText: attributedText)
|
|
subviews.append(label)
|
|
hasSubtitleLabel = true
|
|
return label
|
|
}
|
|
|
|
mutating func addLegacyGroupView(groupThread: TSGroupThread) {
|
|
subviews.append(UIView.spacer(withHeight: 12))
|
|
|
|
let migrationInfo = GroupsV2Migration.migrationInfoForManualMigration(groupThread: groupThread,
|
|
transaction: transaction)
|
|
let legacyGroupView = LegacyGroupView(
|
|
groupThread: groupThread,
|
|
migrationInfo: migrationInfo,
|
|
viewController: delegate
|
|
)
|
|
legacyGroupView.configure()
|
|
legacyGroupView.backgroundColor = delegate.tableViewController.cellBackgroundColor
|
|
subviews.append(legacyGroupView)
|
|
}
|
|
|
|
func buildHeaderSubtitleLabel(attributedText: NSAttributedString) -> OWSLabel {
|
|
let label = OWSLabel()
|
|
|
|
// Defaults need to be set *before* assigning the attributed text,
|
|
// or the attributes will get overriden
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
label.lineBreakMode = .byTruncatingTail
|
|
label.font = .ows_dynamicTypeSubheadlineClamped
|
|
|
|
label.attributedText = attributedText
|
|
|
|
return label
|
|
}
|
|
|
|
func build() -> UIView {
|
|
let header = UIStackView(arrangedSubviews: subviews)
|
|
header.axis = .vertical
|
|
header.alignment = .center
|
|
header.layoutMargins = delegate.tableViewController.cellOuterInsetsWithMargin(bottom: 24)
|
|
header.isLayoutMarginsRelativeArrangement = true
|
|
|
|
header.isUserInteractionEnabled = true
|
|
header.accessibilityIdentifier = UIView.accessibilityIdentifier(in: delegate, name: "mainSectionHeader")
|
|
header.addBackgroundView(withBackgroundColor: delegate.tableViewController.tableBackgroundColor)
|
|
|
|
return header
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
protocol ConversationHeaderDelegate: UIViewController, Dependencies {
|
|
var tableViewController: OWSTableViewController2 { get }
|
|
|
|
var thread: TSThread { get }
|
|
var threadViewModel: ThreadViewModel { get }
|
|
|
|
func threadName(renderLocalUserAsNoteToSelf: Bool, transaction: SDSAnyReadTransaction) -> String
|
|
|
|
var avatarView: UIImageView? { get set }
|
|
|
|
var isBlockedByMigration: Bool { get }
|
|
var canEditConversationAttributes: Bool { get }
|
|
|
|
func tappedAvatar()
|
|
func updateTableContents(shouldReload: Bool)
|
|
func tappedConversationSearch()
|
|
|
|
func startCall(withVideo: Bool)
|
|
|
|
func tappedButton()
|
|
|
|
func didTapUnblockThread(completion: @escaping () -> Void)
|
|
|
|
func didTapAddGroupDescription()
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ConversationHeaderDelegate {
|
|
func threadName(renderLocalUserAsNoteToSelf: Bool, transaction: SDSAnyReadTransaction) -> String {
|
|
var threadName: String
|
|
if thread.isNoteToSelf, !renderLocalUserAsNoteToSelf, let localName = profileManager.localFullName() {
|
|
threadName = localName
|
|
} else {
|
|
threadName = contactsManager.displayName(for: thread, transaction: transaction)
|
|
}
|
|
|
|
if let contactThread = thread as? TSContactThread {
|
|
if let phoneNumber = contactThread.contactAddress.phoneNumber,
|
|
phoneNumber == threadName {
|
|
threadName = PhoneNumber.bestEffortFormatPartialUserSpecifiedText(toLookLikeAPhoneNumber: phoneNumber)
|
|
}
|
|
}
|
|
|
|
return threadName
|
|
}
|
|
|
|
func startCall(withVideo: Bool) {
|
|
guard ConversationViewController.canCall(threadViewModel: threadViewModel) else {
|
|
return owsFailDebug("Tried to start a can when calls are disabled")
|
|
}
|
|
guard withVideo || !thread.isGroupThread else {
|
|
return owsFailDebug("Tried to start an audio only group call")
|
|
}
|
|
|
|
guard !blockingManager.isThreadBlocked(thread) else {
|
|
didTapUnblockThread { [weak self] in
|
|
self?.startCall(withVideo: withVideo)
|
|
}
|
|
return
|
|
}
|
|
|
|
if let currentCall = callService.currentCall {
|
|
if currentCall.thread.uniqueId == thread.uniqueId {
|
|
windowManager.returnToCallView()
|
|
} else {
|
|
owsFailDebug("Tried to start call while call was ongoing")
|
|
}
|
|
} else if let groupThread = thread as? TSGroupThread {
|
|
// We initiated a call, so if there was a pending message request we should accept it.
|
|
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread: thread)
|
|
GroupCallViewController.presentLobby(thread: groupThread)
|
|
} else if let contactThread = thread as? TSContactThread {
|
|
|
|
let didShowSNAlert = SafetyNumberConfirmationSheet.presentIfNecessary(
|
|
address: contactThread.contactAddress,
|
|
confirmationText: CallStrings.confirmAndCallButtonTitle
|
|
) { [weak self] didConfirmIdentity in
|
|
guard didConfirmIdentity else { return }
|
|
self?.startCall(withVideo: withVideo)
|
|
}
|
|
|
|
guard !didShowSNAlert else { return }
|
|
|
|
// We initiated a call, so if there was a pending message request we should accept it.
|
|
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread: thread)
|
|
|
|
outboundIndividualCallInitiator.initiateCall(address: contactThread.contactAddress, isVideo: withVideo)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ConversationSettingsViewController: ConversationHeaderDelegate {
|
|
var tableViewController: OWSTableViewController2 { self }
|
|
|
|
func buildMainHeader() -> UIView {
|
|
ConversationHeaderBuilder.buildHeader(
|
|
for: thread,
|
|
avatarSize: 88,
|
|
options: [.videoCall, .audioCall, .mute, .search, .renderLocalUserAsNoteToSelf],
|
|
delegate: self
|
|
)
|
|
}
|
|
|
|
func tappedButton() {}
|
|
|
|
func didTapAddGroupDescription() {
|
|
guard let groupThread = thread as? TSGroupThread else { return }
|
|
let vc = GroupDescriptionViewController(
|
|
groupModel: groupThread.groupModel,
|
|
options: [.editable, .updateImmediately]
|
|
)
|
|
vc.descriptionDelegate = self
|
|
presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
|
|
}
|
|
}
|
|
|
|
extension ConversationSettingsViewController: GroupDescriptionViewControllerDelegate {
|
|
func groupDescriptionViewControllerDidComplete(groupDescription: String?) {
|
|
reloadThreadAndUpdateContent()
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class OWSLabel: UILabel {
|
|
|
|
// MARK: - Tap
|
|
|
|
public typealias TapBlock = () -> Void
|
|
private var tapBlock: TapBlock?
|
|
|
|
public func addTapGesture(_ tapBlock: @escaping TapBlock) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(self.tapBlock == nil)
|
|
|
|
self.tapBlock = tapBlock
|
|
isUserInteractionEnabled = true
|
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap)))
|
|
}
|
|
|
|
@objc
|
|
private func didTap() {
|
|
guard let tapBlock = tapBlock else {
|
|
owsFailDebug("Missing tapBlock.")
|
|
return
|
|
}
|
|
tapBlock()
|
|
}
|
|
|
|
// MARK: - Long Press
|
|
|
|
public typealias LongPressBlock = () -> Void
|
|
private var longPressBlock: LongPressBlock?
|
|
|
|
public func addLongPressGesture(_ longPressBlock: @escaping LongPressBlock) {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(self.longPressBlock == nil)
|
|
|
|
self.longPressBlock = longPressBlock
|
|
isUserInteractionEnabled = true
|
|
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongPress)))
|
|
}
|
|
|
|
@objc
|
|
private func didLongPress(sender: UIGestureRecognizer) {
|
|
guard sender.state == .began else {
|
|
return
|
|
}
|
|
guard let longPressBlock = longPressBlock else {
|
|
owsFailDebug("Missing longPressBlock.")
|
|
return
|
|
}
|
|
longPressBlock()
|
|
}
|
|
}
|