The old UIButton API is still functional as long as we don't use UIButton.Configuration, so we can safely ignore these warnings until we're ready to adopt the configuration API across the codebase.
731 lines
28 KiB
Swift
731 lines
28 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
public extension ConversationViewController {
|
|
|
|
static func createBanner(title: String,
|
|
bannerColor: UIColor,
|
|
tapBlock: @escaping () -> Void) -> UIView {
|
|
owsAssertDebug(!title.isEmpty)
|
|
|
|
let bannerView = GestureView()
|
|
bannerView.addTap(block: tapBlock)
|
|
bannerView.backgroundColor = bannerColor
|
|
bannerView.accessibilityIdentifier = "banner_close"
|
|
|
|
let label = buildBannerLabel(title: title)
|
|
label.textAlignment = .center
|
|
|
|
let closeIcon = UIImage(imageLiteralResourceName: "x-extra-small")
|
|
let closeButton = UIImageView(image: closeIcon)
|
|
closeButton.tintColor = .white
|
|
bannerView.addSubview(closeButton)
|
|
let kBannerCloseButtonPadding: CGFloat = 8
|
|
closeButton.autoPinEdge(toSuperviewEdge: .top, withInset: kBannerCloseButtonPadding)
|
|
closeButton.autoPinTrailingToSuperviewMargin(withInset: kBannerCloseButtonPadding)
|
|
closeButton.autoSetDimensions(to: closeIcon.size)
|
|
|
|
bannerView.addSubview(label)
|
|
label.autoPinEdge(toSuperviewEdge: .top, withInset: 5)
|
|
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 5)
|
|
let kBannerHPadding: CGFloat = 15
|
|
label.autoPinLeadingToSuperviewMargin(withInset: kBannerHPadding)
|
|
let kBannerHSpacing: CGFloat = 10
|
|
closeButton.autoPinTrailing(toEdgeOf: label, offset: kBannerHSpacing)
|
|
|
|
return bannerView
|
|
}
|
|
|
|
// MARK: - Pending Join Requests Banner
|
|
|
|
func createPendingJoinRequestBanner(
|
|
viewState: CVViewState,
|
|
pendingMemberRequests: Set<SignalServiceAddress>,
|
|
viewMemberRequestsBlock: @escaping () -> Void
|
|
) -> UIView {
|
|
owsAssertDebug(!pendingMemberRequests.isEmpty)
|
|
|
|
let format = OWSLocalizedString("PENDING_GROUP_MEMBERS_REQUEST_BANNER_%d", tableName: "PluralAware",
|
|
comment: "Format for banner indicating that there are pending member requests to join the group. Embeds {{ the number of pending member requests }}.")
|
|
let title = String.localizedStringWithFormat(format, pendingMemberRequests.count)
|
|
|
|
let dismissButton = OWSButton(title: CommonStrings.dismissButton) { [weak self] in
|
|
guard let self = self else { return }
|
|
AssertIsOnMainThread()
|
|
|
|
self.databaseStorage.write { transaction in
|
|
viewState.hidePendingMemberRequestsBanner(
|
|
currentPendingMembers: pendingMemberRequests,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
|
|
self.ensureBannerState()
|
|
}
|
|
dismissButton.titleLabel?.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
|
|
let viewRequestsLabel = OWSLocalizedString("PENDING_GROUP_MEMBERS_REQUEST_BANNER_VIEW_REQUESTS",
|
|
comment: "Label for the 'view requests' button in the pending member requests banner.")
|
|
let viewRequestsButton = OWSButton(title: viewRequestsLabel, block: viewMemberRequestsBlock)
|
|
viewRequestsButton.titleLabel?.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
|
|
|
|
return Self.createBanner(title: title,
|
|
buttons: [dismissButton, viewRequestsButton],
|
|
accessibilityIdentifier: "pending_group_request_banner")
|
|
}
|
|
|
|
// MARK: - Name collision banners
|
|
|
|
func fetchAvatar(
|
|
for address: SignalServiceAddress,
|
|
tx: SDSAnyReadTransaction
|
|
) -> UIImage? {
|
|
if
|
|
address.isLocalAddress,
|
|
let profileAvatar = self.profileManager.localProfileAvatarImage()
|
|
{
|
|
return profileAvatar.resizedImage(to: CGSize(square: 24))
|
|
}
|
|
return Self.avatarBuilder.avatarImage(
|
|
forAddress: address,
|
|
diameterPoints: 24,
|
|
localUserDisplayMode: .asUser,
|
|
transaction: tx
|
|
)
|
|
}
|
|
|
|
func createMessageRequestNameCollisionBannerIfNecessary(viewState: CVViewState) -> UIView? {
|
|
guard let contactThread = thread as? TSContactThread else { return nil }
|
|
|
|
let collisionFinder = ContactThreadNameCollisionFinder
|
|
.makeToCheckMessageRequestNameCollisions(forContactThread: contactThread)
|
|
|
|
guard let (avatar1, avatar2) = databaseStorage.read(block: { tx -> (UIImage?, UIImage?)? in
|
|
guard
|
|
viewState.shouldShowMessageRequestNameCollisionBanner(transaction: tx),
|
|
let collision = collisionFinder.findCollisions(transaction: tx).first
|
|
else { return nil }
|
|
|
|
return (
|
|
fetchAvatar(for: collision.elements[0].address, tx: tx),
|
|
fetchAvatar(for: collision.elements[1].address, tx: tx)
|
|
)
|
|
}) else { return nil }
|
|
|
|
let banner = NameCollisionBanner()
|
|
banner.labelText = OWSLocalizedString(
|
|
"MESSAGE_REQUEST_NAME_COLLISION_BANNER_LABEL",
|
|
comment: "Banner label notifying user that a new message is from a user with the same name as an existing contact")
|
|
banner.reviewActionText = CommonStrings.viewButton
|
|
if let avatar1 = avatar1, let avatar2 = avatar2 {
|
|
banner.primaryImage = avatar1
|
|
banner.secondaryImage = avatar2
|
|
}
|
|
|
|
banner.closeAction = { [weak self] in
|
|
guard let self = self else { return }
|
|
self.databaseStorage.write { viewState.hideMessageRequestNameCollisionBanner(transaction: $0) }
|
|
self.ensureBannerState()
|
|
}
|
|
|
|
banner.reviewAction = { [weak self] in
|
|
guard let self = self else { return }
|
|
let vc = NameCollisionResolutionViewController(collisionFinder: collisionFinder, collisionDelegate: self)
|
|
vc.present(fromViewController: self)
|
|
}
|
|
|
|
return banner
|
|
}
|
|
|
|
func createGroupMembershipCollisionBannerIfNecessary() -> UIView? {
|
|
guard let groupThread = thread as? TSGroupThread else { return nil }
|
|
|
|
// Collision discovery can be expensive, so we only build our banner if
|
|
// we've already done the expensive bit
|
|
guard let collisionFinder = viewState.groupNameCollisionFinder else {
|
|
let collisionFinder = GroupMembershipNameCollisionFinder(thread: groupThread)
|
|
viewState.groupNameCollisionFinder = collisionFinder
|
|
|
|
firstly(on: DispatchQueue.sharedUserInitiated) {
|
|
self.databaseStorage.read { readTx in
|
|
// Prewarm our collision finder off the main thread
|
|
_ = collisionFinder.findCollisions(transaction: readTx)
|
|
}
|
|
}.done(on: DispatchQueue.main) {
|
|
self.ensureBannerState()
|
|
}.catch { error in
|
|
owsFailDebug("\(error)")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
guard collisionFinder.hasFetchedProfileUpdateMessages else {
|
|
// We already have a collision finder. It just hasn't finished fetching.
|
|
return nil
|
|
}
|
|
|
|
// Fetch the necessary info to build the banner
|
|
guard let (title, avatar1, avatar2) = databaseStorage.read(block: { readTx -> (String, UIImage?, UIImage?)? in
|
|
let collisionSets = collisionFinder.findCollisions(transaction: readTx)
|
|
guard !collisionSets.isEmpty else { return nil }
|
|
|
|
let totalCollisionElementCount = collisionSets.reduce(0) { $0 + $1.elements.count }
|
|
|
|
let title: String
|
|
|
|
if collisionSets.count > 1 {
|
|
let titleFormat = OWSLocalizedString(
|
|
"GROUP_MEMBERSHIP_MULTIPLE_COLLISIONS_BANNER_TITLE_%d",
|
|
tableName: "PluralAware",
|
|
comment: "Banner title alerting user to a name collision set ub the group membership. Embeds {{ total number of colliding members }}"
|
|
)
|
|
title = String.localizedStringWithFormat(titleFormat, collisionSets.count)
|
|
} else {
|
|
let titleFormat = OWSLocalizedString(
|
|
"GROUP_MEMBERSHIP_COLLISIONS_BANNER_TITLE_%d",
|
|
tableName: "PluralAware",
|
|
comment: "Banner title alerting user about multiple name collisions in group membership. Embeds {{ number of sets of colliding members }}"
|
|
)
|
|
title = String.localizedStringWithFormat(titleFormat, totalCollisionElementCount)
|
|
}
|
|
|
|
let avatar1 = fetchAvatar(
|
|
for: collisionSets[0].elements[0].address,
|
|
tx: readTx
|
|
)
|
|
let avatar2 = fetchAvatar(
|
|
for: collisionSets[0].elements[1].address,
|
|
tx: readTx
|
|
)
|
|
return (title, avatar1, avatar2)
|
|
|
|
}) else { return nil }
|
|
|
|
let banner = NameCollisionBanner()
|
|
banner.labelText = title
|
|
banner.reviewActionText = CommonStrings.viewButton
|
|
if let avatar1 = avatar1, let avatar2 = avatar2 {
|
|
banner.primaryImage = avatar1
|
|
banner.secondaryImage = avatar2
|
|
}
|
|
|
|
banner.closeAction = { [weak self] in
|
|
self?.databaseStorage.asyncWrite(block: { writeTx in
|
|
collisionFinder.markCollisionsAsResolved(transaction: writeTx)
|
|
}, completion: {
|
|
self?.ensureBannerState()
|
|
})
|
|
}
|
|
|
|
banner.reviewAction = { [weak self] in
|
|
guard let self = self else { return }
|
|
let vc = NameCollisionResolutionViewController(collisionFinder: collisionFinder, collisionDelegate: self)
|
|
vc.present(fromViewController: self)
|
|
}
|
|
|
|
return banner
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
fileprivate extension ConversationViewController {
|
|
|
|
static func buildBannerLabel(title: String) -> UILabel {
|
|
let label = UILabel()
|
|
label.font = UIFont.dynamicTypeSubheadlineClamped.semibold()
|
|
label.text = title
|
|
label.textColor = .white
|
|
label.numberOfLines = 0
|
|
label.lineBreakMode = .byWordWrapping
|
|
return label
|
|
}
|
|
|
|
static func createBanner(title: String,
|
|
buttons: [UIView],
|
|
accessibilityIdentifier: String) -> UIView {
|
|
|
|
let titleLabel = buildBannerLabel(title: title)
|
|
titleLabel.font = .dynamicTypeSubheadlineClamped
|
|
|
|
let buttonRow = UIStackView(arrangedSubviews: [UIView.hStretchingSpacer()] + buttons)
|
|
buttonRow.axis = .horizontal
|
|
buttonRow.spacing = 24
|
|
|
|
let bannerView = UIStackView(arrangedSubviews: [ titleLabel, buttonRow ])
|
|
bannerView.axis = .vertical
|
|
bannerView.alignment = .fill
|
|
bannerView.spacing = 10
|
|
bannerView.layoutMargins = UIEdgeInsets(top: 14, leading: 16, bottom: 14, trailing: 16)
|
|
bannerView.isLayoutMarginsRelativeArrangement = true
|
|
bannerView.addBackgroundView(withBackgroundColor: .ows_accentBlue)
|
|
bannerView.accessibilityIdentifier = accessibilityIdentifier
|
|
return bannerView
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// A convenience view that allows block-based gesture handling.
|
|
public class GestureView: UIView {
|
|
public init() {
|
|
super.init(frame: .zero)
|
|
|
|
self.layoutMargins = .zero
|
|
}
|
|
|
|
@available(*, unavailable, message: "use other constructor instead.")
|
|
required init?(coder aDecoder: NSCoder) {
|
|
super.init(coder: aDecoder)
|
|
}
|
|
|
|
public typealias BlockType = () -> Void
|
|
|
|
private var tapBlock: BlockType?
|
|
|
|
public func addTap(block tapBlock: @escaping () -> Void) {
|
|
owsAssertDebug(self.tapBlock == nil)
|
|
|
|
self.tapBlock = tapBlock
|
|
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap(_:))))
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
@objc
|
|
func didTap(_ sender: UITapGestureRecognizer) {
|
|
guard sender.state == .recognized else {
|
|
return
|
|
}
|
|
guard let tapBlock = tapBlock else {
|
|
owsFailDebug("Missing tapBlock.")
|
|
return
|
|
}
|
|
tapBlock()
|
|
}
|
|
}
|
|
|
|
private class NameCollisionBanner: UIView {
|
|
|
|
private enum Constants {
|
|
static let avatarSize = CGSize(square: 24)
|
|
static let avatarBorderSize: CGFloat = 2
|
|
}
|
|
|
|
var primaryImage: UIImage? {
|
|
get { primaryImageView.image }
|
|
set {
|
|
primaryImageView.image = newValue
|
|
setNeedsUpdateConstraints()
|
|
}
|
|
}
|
|
|
|
var secondaryImage: UIImage? {
|
|
get { secondaryImageView.image }
|
|
set {
|
|
secondaryImageView.image = newValue
|
|
setNeedsUpdateConstraints()
|
|
}
|
|
}
|
|
|
|
var labelText: String? {
|
|
get { label.text }
|
|
set { label.text = newValue }
|
|
}
|
|
|
|
var reviewActionText: String? {
|
|
get { reviewButton.title(for: .normal) }
|
|
set { reviewButton.setTitle(newValue, for: .normal) }
|
|
}
|
|
|
|
var reviewAction: () -> Void {
|
|
get { reviewButton.block }
|
|
set { reviewButton.block = newValue }
|
|
}
|
|
|
|
var closeAction: () -> Void {
|
|
get { closeButton.block }
|
|
set { closeButton.block = newValue }
|
|
}
|
|
|
|
private let label: UILabel = {
|
|
let label = UILabel()
|
|
label.numberOfLines = 0
|
|
label.font = UIFont.dynamicTypeFootnoteClamped
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
return label
|
|
}()
|
|
|
|
private let primaryImageView: UIImageView = {
|
|
let avatarSize = Constants.avatarSize
|
|
|
|
let imageView = UIImageView.withTemplateImageName("info", tintColor: Theme.secondaryTextAndIconColor)
|
|
|
|
imageView.layer.cornerRadius = avatarSize.smallerAxis / 2
|
|
imageView.layer.masksToBounds = true
|
|
|
|
imageView.autoSetDimensions(to: avatarSize)
|
|
imageView.setCompressionResistanceHigh()
|
|
imageView.setContentHuggingHigh()
|
|
return imageView
|
|
}()
|
|
|
|
private class SecondaryImageView: UIImageView {
|
|
override func layoutSubviews() {
|
|
layer.cornerRadius = Constants.avatarSize.smallerAxis / 2
|
|
layer.masksToBounds = true
|
|
|
|
// Mask out a border around the primary avatar.
|
|
// The background is a UIVisualEffect, so we can't just rely
|
|
// on adding a border around the primary avatar itself.
|
|
let borderSize = Constants.avatarBorderSize
|
|
|
|
let maskPath = UIBezierPath(rect: bounds)
|
|
|
|
let circlePath = UIBezierPath(
|
|
ovalIn: .init(
|
|
origin: .init(
|
|
x: self.width / 2 - borderSize,
|
|
y: self.height / 2 - borderSize
|
|
),
|
|
size: self.frame.size.plus(.square(borderSize * 2))
|
|
)
|
|
)
|
|
|
|
maskPath.append(circlePath)
|
|
|
|
let maskLayer = CAShapeLayer()
|
|
maskLayer.path = maskPath.cgPath
|
|
// Mask the inverse of the circle path
|
|
maskLayer.fillRule = .evenOdd
|
|
|
|
self.layer.mask = maskLayer
|
|
}
|
|
}
|
|
|
|
private let secondaryImageView: SecondaryImageView = {
|
|
let imageView = SecondaryImageView()
|
|
imageView.autoSetDimensions(to: Constants.avatarSize)
|
|
imageView.setCompressionResistanceHigh()
|
|
imageView.setContentHuggingHigh()
|
|
return imageView
|
|
}()
|
|
|
|
private let closeButton: OWSButton = {
|
|
let button = OWSButton(
|
|
imageName: "x-20",
|
|
tintColor: Theme.secondaryTextAndIconColor
|
|
)
|
|
button.accessibilityLabel = OWSLocalizedString(
|
|
"BANNER_CLOSE_ACCESSIBILITY_LABEL",
|
|
comment: "Accessibility label for banner close button"
|
|
)
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.setCompressionResistanceHigh()
|
|
button.setContentHuggingHigh()
|
|
return button
|
|
}()
|
|
|
|
private let reviewButton: OWSButton = {
|
|
let button = OWSRoundedButton()
|
|
button.backgroundColor = Theme.isDarkThemeEnabled ? UIColor(white: 1, alpha: 0.16) : UIColor(white: 0, alpha: 0.08)
|
|
button.setTitleColor(Theme.primaryTextColor, for: .normal)
|
|
button.titleLabel?.font = UIFont.dynamicTypeFootnoteClamped.semibold()
|
|
button.dimsWhenHighlighted = true
|
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.ows_contentEdgeInsets = .init(hMargin: 12, vMargin: 6)
|
|
button.setCompressionResistanceHigh()
|
|
button.setContentHuggingLow()
|
|
|
|
return button
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
|
|
let stackView = UIStackView()
|
|
|
|
if UIAccessibility.isReduceTransparencyEnabled {
|
|
stackView.backgroundColor = Theme.secondaryBackgroundColor
|
|
} else {
|
|
let blurEffectView = UIVisualEffectView(effect: Theme.barBlurEffect)
|
|
stackView.addSubview(blurEffectView)
|
|
blurEffectView.autoPinEdgesToSuperviewEdges()
|
|
stackView.clipsToBounds = true
|
|
}
|
|
|
|
stackView.spacing = 12
|
|
stackView.layoutMargins = .init(margin: 12)
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
stackView.axis = .horizontal
|
|
stackView.alignment = .center
|
|
|
|
let imageContainer = UIView.container()
|
|
|
|
imageContainer.addSubview(secondaryImageView)
|
|
imageContainer.addSubview(primaryImageView)
|
|
stackView.addArrangedSubview(imageContainer)
|
|
|
|
// Offsets adjusted in updateConstraints() based on content
|
|
primaryImageViewConstraints = (
|
|
top: primaryImageView.autoPinEdge(.top, to: .top, of: imageContainer, withOffset: 0),
|
|
leading: primaryImageView.autoPinEdge(.leading, to: .leading, of: imageContainer, withOffset: 4)
|
|
)
|
|
primaryImageView.autoPinEdge(toSuperviewEdge: .trailing)
|
|
primaryImageView.autoPinEdge(.bottom, to: .bottom, of: imageContainer)
|
|
// Secondary image is always offset to the top left of the primary image
|
|
secondaryImageView.autoPinEdge(.top, to: .top, of: primaryImageView, withOffset: -12)
|
|
secondaryImageView.autoPinEdge(.leading, to: .leading, of: primaryImageView, withOffset: -12)
|
|
|
|
stackView.addArrangedSubviews([label, reviewButton, closeButton])
|
|
accessibilityElements = [label, reviewButton, closeButton]
|
|
|
|
let cornerRadius: CGFloat = 18
|
|
|
|
let stackContainer = UIView.container()
|
|
stackContainer.addSubview(stackView)
|
|
stackView.autoPinEdgesToSuperviewEdges()
|
|
stackView.addBorder(with: .ows_blackAlpha10)
|
|
// Clip the stack view so the blurred background is clipped
|
|
stackView.layer.cornerRadius = cornerRadius
|
|
stackView.clipsToBounds = true
|
|
// Apply the shadow to the container so it doesn't get clipped out
|
|
stackContainer.layer.cornerRadius = cornerRadius
|
|
stackContainer.setShadow(radius: 8, opacity: 0.2, offset: .init(width: 0, height: 4))
|
|
|
|
let containerView = UIView()
|
|
containerView.layoutMargins = .init(hMargin: 16, vMargin: 8)
|
|
containerView.addSubview(stackContainer)
|
|
stackContainer.autoPinEdgesToSuperviewMargins()
|
|
|
|
self.addSubview(containerView)
|
|
containerView.autoPinEdgesToSuperviewEdges()
|
|
}
|
|
|
|
var primaryImageViewConstraints: (top: NSLayoutConstraint, leading: NSLayoutConstraint)?
|
|
|
|
override func updateConstraints() {
|
|
super.updateConstraints()
|
|
guard let constraints = primaryImageViewConstraints else { return }
|
|
|
|
// If we have a secondary image, we want to adjust our constraints a bit
|
|
let hasSecondaryImage = (secondaryImage != nil)
|
|
constraints.top.constant = hasSecondaryImage ? 12 : 0
|
|
constraints.leading.constant = hasSecondaryImage ? 16 : 4
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ConversationViewController {
|
|
|
|
public func ensureBannerState() {
|
|
AssertIsOnMainThread()
|
|
|
|
// This method should be called rarely, so it's simplest to discard and
|
|
// rebuild the indicator view every time.
|
|
bannerView?.removeFromSuperview()
|
|
self.bannerView = nil
|
|
|
|
var banners = [UIView]()
|
|
|
|
// Most of these banners should hide themselves when the user scrolls
|
|
if !userHasScrolled {
|
|
let message: String?
|
|
let noLongerVerifiedIdentityKeys = databaseStorage.read { tx in self.noLongerVerifiedIdentityKeys(tx: tx) }
|
|
switch noLongerVerifiedIdentityKeys.count {
|
|
case 0:
|
|
message = nil
|
|
|
|
case 1:
|
|
let address = noLongerVerifiedIdentityKeys.first!.key
|
|
let displayName = databaseStorage.read { tx in contactsManager.displayName(for: address, tx: tx).resolvedValue() }
|
|
let format = (isGroupConversation
|
|
? OWSLocalizedString("MESSAGES_VIEW_1_MEMBER_NO_LONGER_VERIFIED_FORMAT",
|
|
comment: "Indicates that one member of this group conversation is no longer verified. Embeds {{user's name or phone number}}.")
|
|
: OWSLocalizedString("MESSAGES_VIEW_CONTACT_NO_LONGER_VERIFIED_FORMAT",
|
|
comment: "Indicates that this 1:1 conversation is no longer verified. Embeds {{user's name or phone number}}."))
|
|
message = String(format: format, displayName)
|
|
|
|
default:
|
|
message = OWSLocalizedString("MESSAGES_VIEW_N_MEMBERS_NO_LONGER_VERIFIED",
|
|
comment: "Indicates that more than one member of this group conversation is no longer verified.")
|
|
}
|
|
if let message {
|
|
let banner = ConversationViewController.createBanner(
|
|
title: message,
|
|
bannerColor: .ows_accentRed,
|
|
tapBlock: { [weak self] in
|
|
self?.noLongerVerifiedBannerViewWasTapped(noLongerVerifiedIdentityKeys: noLongerVerifiedIdentityKeys)
|
|
}
|
|
)
|
|
banners.append(banner)
|
|
}
|
|
|
|
func buildBlockStateMessage() -> String? {
|
|
guard isGroupConversation else {
|
|
return nil
|
|
}
|
|
let blockedGroupMemberCount = self.blockedGroupMemberCount
|
|
if blockedGroupMemberCount > 0 {
|
|
return String.localizedStringWithFormat(
|
|
OWSLocalizedString("MESSAGES_VIEW_GROUP_N_MEMBERS_BLOCKED_%d", tableName: "PluralAware",
|
|
comment: "Indicates that some members of this group has been blocked. Embeds {{the number of blocked users in this group}}."),
|
|
blockedGroupMemberCount)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
if let blockStateMessage = buildBlockStateMessage() {
|
|
let banner = ConversationViewController.createBanner(title: blockStateMessage,
|
|
bannerColor: .ows_accentRed) { [weak self] in
|
|
self?.blockBannerViewWasTapped()
|
|
}
|
|
banners.append(banner)
|
|
}
|
|
|
|
let pendingMemberRequests = self.pendingMemberRequests
|
|
if
|
|
!pendingMemberRequests.isEmpty,
|
|
self.canApprovePendingMemberRequests,
|
|
databaseStorage.read(block: { transaction in
|
|
// We will skip this read if the above checks fail, which
|
|
// will be most of the time.
|
|
viewState.shouldShowPendingMemberRequestsBanner(
|
|
currentPendingMembers: pendingMemberRequests,
|
|
transaction: transaction
|
|
)
|
|
}) {
|
|
let banner = self.createPendingJoinRequestBanner(
|
|
viewState: viewState,
|
|
pendingMemberRequests: pendingMemberRequests
|
|
) { [weak self] in
|
|
self?.showConversationSettingsAndShowMemberRequests()
|
|
}
|
|
|
|
banners.append(banner)
|
|
}
|
|
}
|
|
|
|
if let banner = createMessageRequestNameCollisionBannerIfNecessary(viewState: viewState) {
|
|
banners.append(banner)
|
|
}
|
|
|
|
if let banner = createGroupMembershipCollisionBannerIfNecessary() {
|
|
banners.append(banner)
|
|
}
|
|
|
|
if banners.isEmpty {
|
|
if hasViewDidAppearEverBegun {
|
|
updateContentInsets()
|
|
}
|
|
return
|
|
}
|
|
|
|
let bannerView = UIStackView(arrangedSubviews: banners)
|
|
bannerView.axis = .vertical
|
|
bannerView.alignment = .fill
|
|
self.view.addSubview(bannerView)
|
|
bannerView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
|
bannerView.autoPinEdge(toSuperviewEdge: .leading)
|
|
bannerView.autoPinEdge(toSuperviewEdge: .trailing)
|
|
self.view.layoutSubviews()
|
|
|
|
self.bannerView = bannerView
|
|
if hasViewDidAppearEverBegun {
|
|
updateContentInsets()
|
|
}
|
|
}
|
|
|
|
private var pendingMemberRequests: Set<SignalServiceAddress> {
|
|
if let groupThread = thread as? TSGroupThread {
|
|
return groupThread.groupMembership.requestingMembers
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
private var canApprovePendingMemberRequests: Bool {
|
|
if let groupThread = thread as? TSGroupThread {
|
|
return groupThread.isLocalUserFullMemberAndAdministrator
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func blockBannerViewWasTapped() {
|
|
AssertIsOnMainThread()
|
|
|
|
if isBlockedConversation() {
|
|
// If this a blocked conversation, offer to unblock.
|
|
showUnblockConversationUI(completion: nil)
|
|
} else if isGroupConversation {
|
|
// If this a group conversation with at least one blocked member,
|
|
// Show the block list view.
|
|
let blockedGroupMemberCount = self.blockedGroupMemberCount
|
|
if blockedGroupMemberCount > 0 {
|
|
let vc = BlockListViewController()
|
|
navigationController?.pushViewController(vc, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func noLongerVerifiedBannerViewWasTapped(noLongerVerifiedIdentityKeys: [SignalServiceAddress: Data]) {
|
|
AssertIsOnMainThread()
|
|
|
|
let title: String
|
|
switch noLongerVerifiedIdentityKeys.count {
|
|
case 0:
|
|
return
|
|
case 1:
|
|
title = OWSLocalizedString(
|
|
"VERIFY_PRIVACY",
|
|
comment: "Label for button or row which allows users to verify the safety number of another user."
|
|
)
|
|
default:
|
|
title = OWSLocalizedString(
|
|
"VERIFY_PRIVACY_MULTIPLE",
|
|
comment: "Label for button or row which allows users to verify the safety numbers of multiple users."
|
|
)
|
|
}
|
|
|
|
let actionSheet = ActionSheetController()
|
|
|
|
actionSheet.addAction(ActionSheetAction(title: title, style: .default) { [weak self] _ in
|
|
self?.showNoLongerVerifiedUI(noLongerVerifiedIdentityKeys: noLongerVerifiedIdentityKeys)
|
|
})
|
|
|
|
actionSheet.addAction(ActionSheetAction(title: CommonStrings.dismissButton,
|
|
accessibilityIdentifier: "dismiss",
|
|
style: .cancel) { [weak self] _ in
|
|
self?.resetVerificationStateToDefault(noLongerVerifiedIdentityKeys: noLongerVerifiedIdentityKeys)
|
|
})
|
|
|
|
dismissKeyBoard()
|
|
presentActionSheet(actionSheet)
|
|
}
|
|
|
|
private var blockedGroupMemberCount: Int {
|
|
guard let groupThread = thread as? TSGroupThread else {
|
|
owsFailDebug("Invalid thread.")
|
|
return 0
|
|
}
|
|
|
|
let blockedMembers = databaseStorage.read { readTx in
|
|
groupThread.groupModel.groupMembers.filter {
|
|
blockingManager.isAddressBlocked($0, transaction: readTx)
|
|
}
|
|
}
|
|
return blockedMembers.count
|
|
}
|
|
}
|