604 lines
23 KiB
Swift
604 lines
23 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
protocol MessageRequestDelegate: AnyObject {
|
|
func messageRequestViewDidTapBlock()
|
|
func messageRequestViewDidTapDelete()
|
|
func messageRequestViewDidTapAccept(mode: MessageRequestMode, unblockThread: Bool, unhideRecipient: Bool)
|
|
func messageRequestViewDidTapUnblock(mode: MessageRequestMode)
|
|
func messageRequestViewDidTapReport()
|
|
func messageRequestViewDidTapLearnMore()
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public enum MessageRequestMode: UInt {
|
|
case none
|
|
case contactOrGroupRequest
|
|
case groupInviteRequest
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public struct MessageRequestType: Equatable {
|
|
let isGroupV1Thread: Bool
|
|
let isGroupV2Thread: Bool
|
|
let isThreadBlocked: Bool
|
|
let hasSentMessages: Bool
|
|
let isThreadFromHiddenRecipient: Bool
|
|
let hasReportedSpam: Bool
|
|
let isLocalUserInvitedMember: Bool
|
|
let showReviewRequestsCarefullyWarning: Bool
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
class MessageRequestView: ConversationBottomPanelView {
|
|
|
|
enum LocalizedStrings {
|
|
static let block = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_BLOCK_BUTTON",
|
|
comment: "A button used to block a user on an incoming message request.",
|
|
)
|
|
static let unblock = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_UNBLOCK_BUTTON",
|
|
comment: "A button used to unlock a blocked conversation.",
|
|
)
|
|
static let delete = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_DELETE_BUTTON",
|
|
comment: "incoming message request button text which deletes a conversation",
|
|
)
|
|
static let report = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_REPORT_BUTTON",
|
|
comment: "incoming message request button text which reports a conversation as spam",
|
|
)
|
|
static let accept = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_ACCEPT_BUTTON",
|
|
comment: "A button used to accept a user on an incoming message request.",
|
|
)
|
|
static let `continue` = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_CONTINUE_BUTTON",
|
|
comment: "A button used to continue a conversation and share your profile.",
|
|
)
|
|
}
|
|
|
|
private let thread: TSThread
|
|
private let mode: MessageRequestMode
|
|
private let messageRequestType: MessageRequestType
|
|
|
|
private var isGroupV1Thread: Bool {
|
|
messageRequestType.isGroupV1Thread
|
|
}
|
|
|
|
private var isGroupV2Thread: Bool {
|
|
messageRequestType.isGroupV2Thread
|
|
}
|
|
|
|
private var isThreadBlocked: Bool {
|
|
messageRequestType.isThreadBlocked
|
|
}
|
|
|
|
private var hasSentMessages: Bool {
|
|
messageRequestType.hasSentMessages
|
|
}
|
|
|
|
private var isThreadFromHiddenRecipient: Bool {
|
|
messageRequestType.isThreadFromHiddenRecipient
|
|
}
|
|
|
|
private var hasReportedSpam: Bool {
|
|
messageRequestType.hasReportedSpam
|
|
}
|
|
|
|
private var showReviewRequestsCarefullyWarning: Bool {
|
|
messageRequestType.showReviewRequestsCarefullyWarning
|
|
}
|
|
|
|
weak var delegate: MessageRequestDelegate?
|
|
|
|
// MARK: - ConversationBottomPanelView
|
|
|
|
override var useGlassPanel: Bool {
|
|
false
|
|
}
|
|
|
|
init(threadViewModel: ThreadViewModel) {
|
|
let thread = threadViewModel.threadRecord
|
|
self.thread = thread
|
|
self.messageRequestType = SSKEnvironment.shared.databaseStorageRef.read { transaction in
|
|
Self.messageRequestType(forThread: thread, transaction: transaction)
|
|
}
|
|
|
|
if let groupThread = thread as? TSGroupThread, groupThread.isGroupV2Thread {
|
|
self.mode = (
|
|
groupThread.groupModel.groupMembership.isLocalUserInvitedMember
|
|
? .groupInviteRequest
|
|
: .contactOrGroupRequest,
|
|
)
|
|
} else {
|
|
self.mode = .contactOrGroupRequest
|
|
}
|
|
|
|
super.init(frame: .zero)
|
|
|
|
let arrangedSubviews: [UIView] = {
|
|
switch mode {
|
|
case .none:
|
|
owsFailDebug("Invalid mode.")
|
|
return []
|
|
case .contactOrGroupRequest:
|
|
return [
|
|
prepareMessageRequestPrompt(),
|
|
prepareMessageRequestButtons(),
|
|
]
|
|
case .groupInviteRequest:
|
|
return [
|
|
prepareGroupV2InvitePrompt(),
|
|
prepareGroupV2InviteButtons(),
|
|
]
|
|
}
|
|
}()
|
|
|
|
let stackView = UIStackView(arrangedSubviews: arrangedSubviews)
|
|
stackView.axis = .vertical
|
|
stackView.spacing = 16
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
contentView.addSubview(stackView)
|
|
|
|
addConstraints([
|
|
stackView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
|
|
stackView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor),
|
|
stackView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
|
|
])
|
|
}
|
|
|
|
static func messageRequestType(
|
|
forThread thread: TSThread,
|
|
transaction: DBReadTransaction,
|
|
) -> MessageRequestType {
|
|
let isGroupV1Thread = thread.isGroupV1Thread
|
|
let isGroupV2Thread = thread.isGroupV2Thread
|
|
let isThreadBlocked = SSKEnvironment.shared.blockingManagerRef.isThreadBlocked(thread, transaction: transaction)
|
|
var isThreadFromHiddenRecipient = false
|
|
if let thread = thread as? TSContactThread {
|
|
isThreadFromHiddenRecipient = DependenciesBridge.shared.recipientHidingManager.isHiddenAddress(
|
|
thread.contactAddress,
|
|
tx: transaction,
|
|
)
|
|
}
|
|
let finder = InteractionFinder(threadUniqueId: thread.uniqueId)
|
|
let hasSentMessages = finder.existsOutgoingMessage(transaction: transaction)
|
|
let hasReportedSpam = finder.hasUserReportedSpam(transaction: transaction)
|
|
|
|
var isLocalUserInvitedMember = false
|
|
if let groupThread = thread as? TSGroupThread, groupThread.groupModel.groupMembership.isLocalUserInvitedMember {
|
|
isLocalUserInvitedMember = true
|
|
}
|
|
|
|
var showReviewRequestsCarefullyWarning = false
|
|
if let contactThread = thread as? TSContactThread {
|
|
if isThreadBlocked || isThreadFromHiddenRecipient || hasSentMessages {
|
|
showReviewRequestsCarefullyWarning = false
|
|
} else {
|
|
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(
|
|
for: contactThread.contactAddress,
|
|
tx: transaction,
|
|
)
|
|
switch displayName {
|
|
case .nickname:
|
|
showReviewRequestsCarefullyWarning = false
|
|
default:
|
|
showReviewRequestsCarefullyWarning = true
|
|
}
|
|
}
|
|
} else {
|
|
showReviewRequestsCarefullyWarning = isLocalUserInvitedMember
|
|
}
|
|
|
|
return MessageRequestType(
|
|
isGroupV1Thread: isGroupV1Thread,
|
|
isGroupV2Thread: isGroupV2Thread,
|
|
isThreadBlocked: isThreadBlocked,
|
|
hasSentMessages: hasSentMessages,
|
|
isThreadFromHiddenRecipient: isThreadFromHiddenRecipient,
|
|
hasReportedSpam: hasReportedSpam,
|
|
isLocalUserInvitedMember: isLocalUserInvitedMember,
|
|
showReviewRequestsCarefullyWarning: showReviewRequestsCarefullyWarning,
|
|
)
|
|
}
|
|
|
|
required init(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
// MARK: - Message Request
|
|
|
|
// This is used for:
|
|
//
|
|
// * Contact threads
|
|
// * v1 groups
|
|
// * v2 groups if user does not have a pending invite.
|
|
func prepareMessageRequestPrompt() -> UITextView {
|
|
if thread.isGroupThread {
|
|
let string: String
|
|
var appendLearnMoreLink = false
|
|
if thread.isGroupV1Thread {
|
|
if isThreadBlocked {
|
|
string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_BLOCKED_GROUP_PROMPT",
|
|
comment: "A prompt notifying that the user must unblock this group to continue.",
|
|
)
|
|
} else if hasSentMessages {
|
|
string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_EXISTING_GROUP_PROMPT",
|
|
comment: "A prompt notifying that the user must share their profile with this group.",
|
|
)
|
|
appendLearnMoreLink = true
|
|
} else {
|
|
string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_NEW_GROUP_PROMPT",
|
|
comment: "A prompt asking if the user wants to accept a group invite.",
|
|
)
|
|
}
|
|
} else {
|
|
owsAssertDebug(thread.isGroupV2Thread)
|
|
|
|
if isThreadBlocked {
|
|
string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_BLOCKED_GROUP_PROMPT_V2",
|
|
comment: "A prompt notifying that the user must unblock this group to continue.",
|
|
)
|
|
} else {
|
|
string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_NEW_GROUP_PROMPT_V2",
|
|
comment: "A prompt asking if the user wants to accept a group invite.",
|
|
)
|
|
}
|
|
}
|
|
|
|
return prepareTextView(
|
|
attributedString: NSAttributedString(string: string, attributes: [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped,
|
|
.foregroundColor: UIColor.Signal.label,
|
|
]),
|
|
appendLearnMoreLink: appendLearnMoreLink,
|
|
)
|
|
} else if let thread = thread as? TSContactThread {
|
|
let formatString: String
|
|
var appendLearnMoreLink = false
|
|
|
|
if isThreadBlocked {
|
|
formatString = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_BLOCKED_CONTACT_PROMPT_FORMAT",
|
|
comment: "A prompt notifying that the user must unblock this conversation to continue. Embeds {{contact name}}.",
|
|
)
|
|
} else if isThreadFromHiddenRecipient {
|
|
formatString = OWSLocalizedString("MESSAGE_REQUEST_VIEW_REMOVED_CONTACT_PROMPT_FORMAT", comment: "A prompt asking if the user wants to accept a conversation invite from a person whom they previously removed. Embeds {{contact name}}.")
|
|
|
|
} else if hasSentMessages {
|
|
formatString = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_EXISTING_CONTACT_PROMPT_FORMAT",
|
|
comment: "A prompt notifying that the user must share their profile with this conversation. Embeds {{contact name}}.",
|
|
)
|
|
appendLearnMoreLink = true
|
|
} else {
|
|
let attrString = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_NEW_CONTACT_PROMPT",
|
|
comment: "A prompt asking if the user wants to accept a conversation invite.",
|
|
)
|
|
return preparePromptTextView(prompt: attrString)
|
|
}
|
|
|
|
let shortName = SSKEnvironment.shared.databaseStorageRef.read { transaction in
|
|
return SSKEnvironment.shared.contactManagerRef.displayName(for: thread.contactAddress, tx: transaction).resolvedValue(useShortNameIfAvailable: true)
|
|
}
|
|
|
|
return preparePromptTextView(
|
|
formatString: formatString,
|
|
embeddedString: shortName,
|
|
appendLearnMoreLink: appendLearnMoreLink,
|
|
)
|
|
} else {
|
|
owsFailDebug("unexpected thread type")
|
|
return UITextView()
|
|
}
|
|
}
|
|
|
|
// This is used for:
|
|
//
|
|
// * Contact threads
|
|
// * v1 groups
|
|
// * v2 groups if user does not have a pending invite.
|
|
func prepareMessageRequestButtons() -> UIStackView {
|
|
let mode = self.mode
|
|
var buttons = [UIView]()
|
|
|
|
if isThreadBlocked {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.delete, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapDelete()
|
|
},
|
|
)
|
|
if !hasReportedSpam {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.report, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapReport()
|
|
},
|
|
)
|
|
}
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.unblock) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapUnblock(mode: mode)
|
|
},
|
|
)
|
|
} else if isThreadFromHiddenRecipient {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.block, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapBlock()
|
|
},
|
|
)
|
|
if !hasReportedSpam {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.report, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapReport()
|
|
},
|
|
)
|
|
} else {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.delete, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapDelete()
|
|
},
|
|
)
|
|
}
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.accept) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapAccept(mode: mode, unblockThread: false, unhideRecipient: true)
|
|
},
|
|
)
|
|
} else if hasSentMessages {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.block, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapBlock()
|
|
},
|
|
)
|
|
if !hasReportedSpam {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.report, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapReport()
|
|
},
|
|
)
|
|
} else {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.delete, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapDelete()
|
|
},
|
|
)
|
|
}
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.continue) { [weak self] in
|
|
// This is the same action as accepting the message request, but displays
|
|
// with slightly different visuals if the user has already been messaging
|
|
// this user in the past but didn't share their profile.
|
|
self?.delegate?.messageRequestViewDidTapAccept(mode: mode, unblockThread: false, unhideRecipient: false)
|
|
},
|
|
)
|
|
} else {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.block, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapBlock()
|
|
},
|
|
)
|
|
if !hasReportedSpam {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.report, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapReport()
|
|
},
|
|
)
|
|
} else {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.delete, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapDelete()
|
|
},
|
|
)
|
|
}
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.accept) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapAccept(mode: mode, unblockThread: false, unhideRecipient: false)
|
|
},
|
|
)
|
|
}
|
|
|
|
return prepareButtonStack(buttons)
|
|
}
|
|
|
|
// MARK: - Group V2 Invites
|
|
|
|
func prepareGroupV2InvitePrompt() -> UITextView {
|
|
let string = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_VIEW_NEW_GROUP_PROMPT",
|
|
comment: "A prompt asking if the user wants to accept a group invite.",
|
|
)
|
|
|
|
let centered = NSMutableParagraphStyle()
|
|
centered.alignment = .center
|
|
centered.paragraphSpacingBefore = 8
|
|
|
|
return prepareTextView(
|
|
attributedString: NSAttributedString(string: string, attributes: [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped,
|
|
.foregroundColor: UIColor.Signal.label,
|
|
.paragraphStyle: centered,
|
|
]),
|
|
appendLearnMoreLink: false,
|
|
)
|
|
}
|
|
|
|
func prepareGroupV2InviteButtons() -> UIStackView {
|
|
let mode = self.mode
|
|
var buttons = [UIView]()
|
|
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.block, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapBlock()
|
|
},
|
|
)
|
|
|
|
if !hasReportedSpam {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.report, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapReport()
|
|
},
|
|
)
|
|
} else {
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.delete, destructive: true) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapDelete()
|
|
},
|
|
)
|
|
}
|
|
|
|
buttons.append(
|
|
prepareButton(title: LocalizedStrings.accept) { [weak self] in
|
|
self?.delegate?.messageRequestViewDidTapAccept(mode: mode, unblockThread: false, unhideRecipient: false)
|
|
},
|
|
)
|
|
return prepareButtonStack(buttons)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private func buttonConfiguration(title: String) -> UIButton.Configuration {
|
|
var configuration: UIButton.Configuration
|
|
if #available(iOS 26, *) {
|
|
configuration = .prominentGlass()
|
|
configuration.baseForegroundColor = .Signal.label
|
|
} else {
|
|
configuration = .plain()
|
|
configuration.baseForegroundColor = .Signal.accent
|
|
}
|
|
configuration.titleAlignment = .center
|
|
configuration.titleTextAttributesTransformer = .defaultFont(.dynamicTypeHeadlineClamped)
|
|
configuration.baseBackgroundColor = .clear
|
|
if #available(iOS 26, *) {
|
|
configuration.cornerStyle = .capsule
|
|
}
|
|
configuration.title = title
|
|
configuration.contentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 8)
|
|
return configuration
|
|
}
|
|
|
|
private func prepareButton(
|
|
title: String,
|
|
destructive: Bool = false,
|
|
actionBlock: @escaping () -> Void,
|
|
) -> UIButton {
|
|
let button = UIButton(
|
|
configuration: buttonConfiguration(title: title),
|
|
primaryAction: UIAction { _ in
|
|
actionBlock()
|
|
},
|
|
)
|
|
if destructive {
|
|
button.configuration?.baseForegroundColor = .Signal.red
|
|
}
|
|
return button
|
|
}
|
|
|
|
private func preparePromptTextView(prompt: String) -> UITextView {
|
|
let centered = NSMutableParagraphStyle()
|
|
centered.alignment = .center
|
|
if showReviewRequestsCarefullyWarning {
|
|
centered.paragraphSpacingBefore = 8
|
|
}
|
|
let defaultAttributes: AttributedFormatArg.Attributes = [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped,
|
|
.foregroundColor: UIColor.Signal.label,
|
|
.paragraphStyle: centered,
|
|
]
|
|
|
|
let attrString = NSAttributedString(string: prompt, attributes: defaultAttributes)
|
|
return prepareTextView(attributedString: attrString, appendLearnMoreLink: false)
|
|
}
|
|
|
|
private func preparePromptTextView(formatString: String, embeddedString: String, appendLearnMoreLink: Bool) -> UITextView {
|
|
let centered = NSMutableParagraphStyle()
|
|
centered.alignment = .center
|
|
let defaultAttributes: AttributedFormatArg.Attributes = [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped,
|
|
.foregroundColor: UIColor.Signal.label,
|
|
.paragraphStyle: centered,
|
|
]
|
|
|
|
let attributesForEmbedded: AttributedFormatArg.Attributes = [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped.semibold(),
|
|
.foregroundColor: UIColor.Signal.label,
|
|
]
|
|
|
|
let attributedString = NSAttributedString.make(
|
|
fromFormat: formatString,
|
|
attributedFormatArgs: [.string(embeddedString, attributes: attributesForEmbedded)],
|
|
defaultAttributes: defaultAttributes,
|
|
)
|
|
|
|
return prepareTextView(attributedString: attributedString, appendLearnMoreLink: appendLearnMoreLink)
|
|
}
|
|
|
|
private func prepareTextView(
|
|
attributedString: NSAttributedString,
|
|
appendLearnMoreLink: Bool,
|
|
) -> UITextView {
|
|
let textView = LinkingTextView()
|
|
if appendLearnMoreLink {
|
|
textView.attributedText = .composed(of: [
|
|
attributedString,
|
|
" ",
|
|
CommonStrings.learnMore.styled(
|
|
with: .link(URL.Support.profilesAndMessageRequests),
|
|
.font(.dynamicTypeSubheadlineClamped),
|
|
),
|
|
])
|
|
.styled(with: .alignment(.center))
|
|
} else if showReviewRequestsCarefullyWarning {
|
|
let fullText = NSMutableAttributedString()
|
|
|
|
let centered = NSMutableParagraphStyle()
|
|
centered.alignment = .center
|
|
let reviewWarningAttributes: AttributedFormatArg.Attributes = [
|
|
.font: UIFont.dynamicTypeSubheadlineClamped.semibold(),
|
|
.foregroundColor: UIColor.Signal.warningLabel,
|
|
.paragraphStyle: centered,
|
|
]
|
|
|
|
let warningIcon = SignalSymbol.errorTriangle.attributedString(
|
|
dynamicTypeBaseSize: UIFont.dynamicTypeSubheadlineClamped.pointSize,
|
|
attributes: [.foregroundColor: UIColor.Signal.warningLabel],
|
|
)
|
|
|
|
let warningLabel = warningIcon + " " + NSAttributedString(
|
|
string: OWSLocalizedString("SYSTEM_MESSAGE_UNKNOWN_THREAD_REVIEW_CAREFULLY_WARNING", comment: "Indicator warning about an unknown contact thread") + "\n",
|
|
attributes: reviewWarningAttributes,
|
|
)
|
|
fullText.append(warningLabel)
|
|
fullText.append(attributedString)
|
|
|
|
textView.attributedText = fullText.styled(with: .alignment(.center))
|
|
} else {
|
|
textView.attributedText = attributedString.styled(with: .alignment(.center))
|
|
}
|
|
return textView
|
|
}
|
|
|
|
private func prepareButtonStack(_ buttons: [UIView]) -> UIStackView {
|
|
let buttonsStack = UIStackView(arrangedSubviews: buttons)
|
|
buttonsStack.spacing = 11.5
|
|
buttonsStack.distribution = .fillEqually
|
|
return buttonsStack
|
|
}
|
|
}
|