TM-SGNL-iOS/Signal/ConversationView/Components/CVComponentThreadDetails.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

915 lines
37 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalServiceKit
public import SignalUI
public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
public var componentKey: CVComponentKey { .threadDetails }
public var cellReuseIdentifier: CVCellReuseIdentifier {
CVCellReuseIdentifier.threadDetails
}
public let isDedicatedCell = false
private let threadDetails: CVComponentState.ThreadDetails
private var avatarDataSource: ConversationAvatarDataSource? { threadDetails.avatarDataSource }
private var titleText: String { threadDetails.titleText }
private var bioText: String? { threadDetails.bioText }
private var detailsText: String? { threadDetails.detailsText }
private var mutualGroupsText: NSAttributedString? { threadDetails.mutualGroupsText }
private var shouldShowSafetyTip: Bool { threadDetails.mutualGroupsTapAction != nil }
private var groupDescriptionText: String? { threadDetails.groupDescriptionText }
private var canTapTitle: Bool {
thread is TSContactThread && !thread.isNoteToSelf
}
init(itemModel: CVItemModel, threadDetails: CVComponentState.ThreadDetails) {
self.threadDetails = threadDetails
super.init(itemModel: itemModel)
}
public func configureCellRootComponent(cellView: UIView,
cellMeasurement: CVCellMeasurement,
componentDelegate: CVComponentDelegate,
messageSwipeActionState: CVMessageSwipeActionState,
componentView: CVComponentView) {
Self.configureCellRootComponent(rootComponent: self,
cellView: cellView,
cellMeasurement: cellMeasurement,
componentDelegate: componentDelegate,
componentView: componentView)
}
public func buildComponentView(componentDelegate: CVComponentDelegate) -> CVComponentView {
CVComponentViewThreadDetails()
}
public override func wallpaperBlurView(componentView: CVComponentView) -> CVWallpaperBlurView? {
guard let componentView = componentView as? CVComponentViewThreadDetails else {
owsFailDebug("Unexpected componentView.")
return nil
}
return componentView.wallpaperBlurView
}
public func configureForRendering(componentView componentViewParam: CVComponentView,
cellMeasurement: CVCellMeasurement,
componentDelegate: CVComponentDelegate) {
guard let componentView = componentViewParam as? CVComponentViewThreadDetails else {
owsFailDebug("Unexpected componentView.")
componentViewParam.reset()
return
}
let outerStackView = componentView.outerStackView
let innerStackView = componentView.innerStackView
innerStackView.reset()
outerStackView.reset()
outerStackView.insetsLayoutMarginsFromSafeArea = false
innerStackView.insetsLayoutMarginsFromSafeArea = false
var innerViews = [UIView]()
let avatarView = ConversationAvatarView(sizeClass: avatarSizeClass, localUserDisplayMode: .asUser, useAutolayout: false)
avatarView.updateWithSneakyTransactionIfNecessary { configuration in
configuration.dataSource = avatarDataSource
}
componentView.avatarView = avatarView
if threadDetails.isAvatarBlurred {
let avatarWrapper = ManualLayoutView(name: "avatarWrapper")
avatarWrapper.addSubviewToFillSuperviewEdges(avatarView)
innerViews.append(avatarWrapper)
var unblurAvatarSubviewInfos = [ManualStackSubviewInfo]()
let unblurAvatarIconView = CVImageView()
unblurAvatarIconView.setTemplateImageName("tap-outline-24", tintColor: .ows_white)
unblurAvatarSubviewInfos.append(CGSize.square(24).asManualSubviewInfo(hasFixedSize: true))
let unblurAvatarLabelConfig = CVLabelConfig.unstyledText(
OWSLocalizedString(
"THREAD_DETAILS_TAP_TO_UNBLUR_AVATAR",
comment: "Indicator that a blurred avatar can be revealed by tapping."
),
font: UIFont.dynamicTypeSubheadlineClamped,
textColor: .ows_white
)
let maxWidth = CGFloat(avatarSizeClass.diameter) - 12
let unblurAvatarLabelSize = CVText.measureLabel(config: unblurAvatarLabelConfig, maxWidth: maxWidth)
unblurAvatarSubviewInfos.append(unblurAvatarLabelSize.asManualSubviewInfo)
let unblurAvatarLabel = CVLabel()
unblurAvatarLabelConfig.applyForRendering(label: unblurAvatarLabel)
let unblurAvatarStackConfig = ManualStackView.Config(axis: .vertical,
alignment: .center,
spacing: 8,
layoutMargins: .zero)
let unblurAvatarStackMeasurement = ManualStackView.measure(config: unblurAvatarStackConfig,
subviewInfos: unblurAvatarSubviewInfos)
let unblurAvatarStack = ManualStackView(name: "unblurAvatarStack")
unblurAvatarStack.configure(config: unblurAvatarStackConfig,
measurement: unblurAvatarStackMeasurement,
subviews: [
unblurAvatarIconView,
unblurAvatarLabel
])
avatarWrapper.addSubviewToCenterOnSuperview(unblurAvatarStack,
size: unblurAvatarStackMeasurement.measuredSize)
} else {
innerViews.append(avatarView)
}
innerViews.append(UIView.spacer(withHeight: 1))
if conversationStyle.hasWallpaper {
let wallpaperBlurView = componentView.ensureWallpaperBlurView()
configureWallpaperBlurView(
wallpaperBlurView: wallpaperBlurView,
maskCornerRadius: 24,
componentDelegate: componentDelegate
)
innerStackView.addSubviewToFillSuperviewEdges(wallpaperBlurView)
}
let titleButton = componentView.titleButton
titleLabelConfig.applyForRendering(button: titleButton)
self.configureTitleAction(button: titleButton, delegate: componentDelegate)
innerViews.append(titleButton)
if let bioText = self.bioText {
let bioLabel = componentView.bioLabel
bioLabelConfig(text: bioText).applyForRendering(label: bioLabel)
innerViews.append(UIView.spacer(withHeight: vSpacingSubtitle))
innerViews.append(bioLabel)
}
if let detailsText = self.detailsText {
let detailsLabel = componentView.detailsLabel
detailsLabelConfig(text: detailsText).applyForRendering(label: detailsLabel)
innerViews.append(UIView.spacer(withHeight: vSpacingSubtitle))
innerViews.append(detailsLabel)
}
if let groupDescriptionText = self.groupDescriptionText {
let groupDescriptionPreviewView = componentView.groupDescriptionPreviewView
let config = groupDescriptionTextLabelConfig(text: groupDescriptionText)
groupDescriptionPreviewView.apply(config: config)
groupDescriptionPreviewView.groupName = titleText
innerViews.append(UIView.spacer(withHeight: vSpacingMutualGroups))
innerViews.append(groupDescriptionPreviewView)
}
if let mutualGroupsText = self.mutualGroupsText {
let mutualGroupsLabel = componentView.mutualGroupsLabel
let showTipsButton = componentView.showTipsButton
let groupInfoWrapper = ManualLayoutViewWithLayer(name: "groupWrapper")
var groupInfoSubviewInfos = [ManualStackSubviewInfo]()
innerViews.append(UIView.spacer(withHeight: vSpacingMutualGroups))
if conversationStyle.hasWallpaper {
// Add divider before mutual groups
let divider = UIView()
divider.autoSetDimension(.width, toSize: cellMeasurement.cellSize.width)
divider.autoSetDimension(.height, toSize: 1)
divider.backgroundColor = UIColor(
white: Theme.isDarkThemeEnabled ? 1 : 0,
alpha: 0.12
)
innerViews.append(divider)
} else {
groupInfoWrapper.layer.cornerRadius = 18
groupInfoWrapper.layer.borderWidth = 2
if Theme.isDarkThemeEnabled {
groupInfoWrapper.layer.borderColor = nil
groupInfoWrapper.backgroundColor = UIColor(white: 1, alpha: 0.08)
} else {
groupInfoWrapper.layer.borderColor = UIColor(white: 0, alpha: 0.06).cgColor
groupInfoWrapper.backgroundColor = Theme.backgroundColor
groupInfoWrapper.setShadow(radius: 4, opacity: 0.04, offset: .init(width: 0, height: 2))
groupInfoWrapper.setShadow(radius: 4, opacity: 0.04, offset: .init(width: 0, height: 2))
}
}
innerViews.append(groupInfoWrapper)
let maxWidth = cellMeasurement.cellSize.width
- outerStackConfig.layoutMargins.totalWidth
- innerStackConfig.layoutMargins.totalWidth
- (mutualGroupsPadding * 2)
let mutualGroupsLabelConfig = mutualGroupsLabelConfig(attributedText: mutualGroupsText)
mutualGroupsLabelConfig.applyForRendering(label: mutualGroupsLabel)
let mutualGroupsLabelSize = CVText.measureLabel(config: mutualGroupsLabelConfig, maxWidth: maxWidth)
groupInfoSubviewInfos.append(mutualGroupsLabelSize.asManualSubviewInfo)
var groupInfoSubviews: [UIView] = [ mutualGroupsLabel ]
if self.shouldShowSafetyTip {
groupInfoSubviews.append(showTipsButton)
let safetyButtonLabelConfig = safetyTipsConfig()
safetyButtonLabelConfig.applyForRendering(button: showTipsButton)
showTipsButton.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray60 : .ows_gray05
showTipsButton.ows_contentEdgeInsets = .init(hMargin: 12.0, vMargin: 8.0)
showTipsButton.dimsWhenHighlighted = true
showTipsButton.block = { [weak self] in
self?.didShowTips()
}
groupInfoSubviewInfos.append(showTipsButton.sizeThatFitsMaxSize.asManualSubviewInfo)
}
let groupInfoStackMeasurement = ManualStackView.measure(
config: groupStackConfig,
subviewInfos: groupInfoSubviewInfos
)
let groupInfoStack = ManualStackView(name: "groupInfoStack")
groupInfoStack.configure(
config: groupStackConfig,
measurement: groupInfoStackMeasurement,
subviews: groupInfoSubviews
)
groupInfoWrapper.addSubviewToCenterOnSuperview(
groupInfoStack,
size: groupInfoStackMeasurement.measuredSize
)
}
innerStackView.configure(config: innerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerStack,
subviews: innerViews)
let outerViews = [ innerStackView ]
outerStackView.configure(config: outerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_outerStack,
subviews: outerViews)
}
private let vSpacingSubtitle: CGFloat = 2
private let vSpacingMutualGroups: CGFloat = 16
private let mutualGroupsPadding: CGFloat = 24
private var titleLabelConfig: CVLabelConfig {
let font = UIFont.dynamicTypeTitle1.semibold()
let textColor = Theme.primaryTextColor
let attributedString = NSMutableAttributedString(string: titleText, attributes: [
.font: font,
.foregroundColor: textColor,
])
if threadDetails.shouldShowVerifiedBadge {
attributedString.append(" ")
let verifiedBadgeImage = Theme.iconImage(.official)
let verifiedBadgeAttachment = NSAttributedString.with(
image: verifiedBadgeImage,
font: .dynamicTypeTitle3,
centerVerticallyRelativeTo: font,
heightReference: .pointSize
)
attributedString.append(verifiedBadgeAttachment)
}
if
canTapTitle,
let chevron = UIImage(named: "chevron-right-20")
{
attributedString.append(.with(
image: chevron,
font: .systemFont(ofSize: 24),
attributes: [
.foregroundColor: Theme.primaryIconColor
],
centerVerticallyRelativeTo: font,
heightReference: .pointSize
))
}
return CVLabelConfig.init(
text: .attributedText(attributedString),
displayConfig: .forUnstyledText(font: font, textColor: textColor),
font: font,
textColor: textColor,
numberOfLines: 0,
lineBreakMode: .byWordWrapping,
textAlignment: .center
)
}
private func configureTitleAction(
button: OWSButton,
delegate: CVComponentDelegate?
) {
guard
canTapTitle,
let contactThread = thread as? TSContactThread
else {
button.isEnabled = false
button.dimsWhenHighlighted = false
button.block = {}
return
}
button.dimsWhenHighlighted = true
button.block = { [weak delegate] in
delegate?.didTapContactName(thread: contactThread)
}
button.isEnabled = true
}
private func bioLabelConfig(text: String) -> CVLabelConfig {
CVLabelConfig.unstyledText(
text,
font: .dynamicTypeSubheadline,
textColor: Theme.primaryTextColor,
numberOfLines: 0,
lineBreakMode: .byWordWrapping,
textAlignment: .center
)
}
private func detailsLabelConfig(text: String) -> CVLabelConfig {
CVLabelConfig.unstyledText(
text,
font: .dynamicTypeSubheadline,
textColor: Theme.primaryTextColor,
numberOfLines: 0,
lineBreakMode: .byWordWrapping,
textAlignment: .center
)
}
private static var mutualGroupsFont: UIFont { .dynamicTypeSubheadline }
private static var mutualGroupsTextColor: UIColor { Theme.primaryTextColor }
private func mutualGroupsLabelConfig(attributedText: NSAttributedString) -> CVLabelConfig {
CVLabelConfig(
text: .attributedText(attributedText),
displayConfig: .forUnstyledText(
font: Self.mutualGroupsFont,
textColor: Self.mutualGroupsTextColor
),
font: Self.mutualGroupsFont,
textColor: Self.mutualGroupsTextColor,
numberOfLines: 0,
lineBreakMode: .byWordWrapping,
textAlignment: .center
)
}
private func safetyTipsConfig() -> CVLabelConfig {
CVLabelConfig.unstyledText(
OWSLocalizedString(
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
comment: "Title for Safety Tips button in thread details."
),
font: UIFont.dynamicTypeCaption1.medium(),
textColor: Theme.isDarkThemeEnabled ? .ows_white : .ows_black
)
}
private func groupDescriptionTextLabelConfig(text: String) -> CVLabelConfig {
CVLabelConfig.unstyledText(
text,
font: .dynamicTypeSubheadline,
textColor: Theme.primaryTextColor,
numberOfLines: 2,
lineBreakMode: .byTruncatingTail,
textAlignment: .center
)
}
private static let avatarSizeClass = ConversationAvatarView.Configuration.SizeClass.eightyEight
private var avatarSizeClass: ConversationAvatarView.Configuration.SizeClass { Self.avatarSizeClass }
static func buildComponentState(
thread: TSThread,
transaction: SDSAnyReadTransaction,
avatarBuilder: CVAvatarBuilder
) -> CVComponentState.ThreadDetails {
if let contactThread = thread as? TSContactThread {
return buildComponentState(
contactThread: contactThread,
transaction: transaction,
avatarBuilder: avatarBuilder
)
} else if let groupThread = thread as? TSGroupThread {
return buildComponentState(
groupThread: groupThread,
transaction: transaction,
avatarBuilder: avatarBuilder
)
} else {
owsFailDebug("Invalid thread.")
return CVComponentState.ThreadDetails(
avatarDataSource: nil,
isAvatarBlurred: false,
titleText: TSGroupThread.defaultGroupName,
shouldShowVerifiedBadge: false,
bioText: nil,
detailsText: nil,
mutualGroupsText: nil,
mutualGroupsTapAction: nil,
groupDescriptionText: nil
)
}
}
private static func buildComponentState(
contactThread: TSContactThread,
transaction: SDSAnyReadTransaction,
avatarBuilder: CVAvatarBuilder
) -> CVComponentState.ThreadDetails {
let avatarDataSource = avatarBuilder.buildAvatarDataSource(
forAddress: contactThread.contactAddress,
includingBadge: true,
localUserDisplayMode: .noteToSelf,
diameterPoints: avatarSizeClass.diameter
)
let isAvatarBlurred = SSKEnvironment.shared.contactManagerImplRef.shouldBlurContactAvatar(
contactThread: contactThread,
transaction: transaction
)
let displayName = SSKEnvironment.shared.contactManagerRef.displayName(
for: contactThread.contactAddress,
tx: transaction
)
let titleText = { () -> String in
if contactThread.isNoteToSelf {
return MessageStrings.noteToSelf
} else {
return displayName.resolvedValue()
}
}()
let shouldShowVerifiedBadge = contactThread.isNoteToSelf
let bioText = { () -> String? in
if contactThread.isNoteToSelf {
return nil
}
return SSKEnvironment.shared.profileManagerImplRef.profileBioForDisplay(for: contactThread.contactAddress,
transaction: transaction)
}()
let detailsText = { () -> String? in
guard contactThread.isNoteToSelf else { return nil }
return OWSLocalizedString(
"THREAD_DETAILS_NOTE_TO_SELF_EXPLANATION",
comment: "Subtitle appearing at the top of the users 'note to self' conversation"
)
}()
let (mutualGroupsText, shouldShowSafetyTip) = Self.buildMutualGroupsContactString(
for: displayName,
in: contactThread,
tx: transaction
)
return CVComponentState.ThreadDetails(
avatarDataSource: avatarDataSource,
isAvatarBlurred: isAvatarBlurred,
titleText: titleText,
shouldShowVerifiedBadge: shouldShowVerifiedBadge,
bioText: bioText,
detailsText: detailsText,
mutualGroupsText: mutualGroupsText,
mutualGroupsTapAction: shouldShowSafetyTip ? .showContactSafetyTip : nil,
groupDescriptionText: nil
)
}
private static func buildComponentState(
groupThread: TSGroupThread,
transaction: SDSAnyReadTransaction,
avatarBuilder: CVAvatarBuilder
) -> CVComponentState.ThreadDetails {
// If we need to reload this cell to reflect changes to any of the
// state captured here, we need update the didThreadDetailsChange().
let avatarDataSource = avatarBuilder.buildAvatarDataSource(
forGroupThread: groupThread,
diameterPoints: avatarSizeClass.diameter)
let isAvatarBlurred = SSKEnvironment.shared.contactManagerImplRef.shouldBlurGroupAvatar(
groupThread: groupThread,
transaction: transaction
)
let titleText = groupThread.groupNameOrDefault
let detailsText = { () -> String? in
if let groupModelV2 = groupThread.groupModel as? TSGroupModelV2,
groupModelV2.isJoinRequestPlaceholder {
// Don't show details for a placeholder.
return nil
}
let memberCount = groupThread.groupModel.groupMembership.fullMembers.count
return GroupViewUtils.formatGroupMembersLabel(memberCount: memberCount)
}()
let mutualGroupsText = Self.buildMutualGroupsGroupString(
from: groupThread,
tx: transaction
)
let descriptionText: String? = {
guard let groupModelV2 = groupThread.groupModel as? TSGroupModelV2 else { return nil }
return groupModelV2.descriptionText
}()
return CVComponentState.ThreadDetails(
avatarDataSource: avatarDataSource,
isAvatarBlurred: isAvatarBlurred,
titleText: titleText,
shouldShowVerifiedBadge: false,
bioText: nil,
detailsText: detailsText,
mutualGroupsText: mutualGroupsText,
mutualGroupsTapAction: mutualGroupsText == nil ? nil : .showGroupSafetyTip,
groupDescriptionText: descriptionText
)
}
private var outerStackConfig: CVStackViewConfig {
CVStackViewConfig(
axis: .vertical,
alignment: .fill,
spacing: 0,
layoutMargins: UIEdgeInsets(top: 8, left: 32, bottom: 16, right: 32)
)
}
private var innerStackConfig: CVStackViewConfig {
CVStackViewConfig(
axis: .vertical,
alignment: .center,
spacing: 3,
layoutMargins: UIEdgeInsets(top: 20, leading: 16, bottom: 24, trailing: 16)
)
}
private var groupStackConfig: CVStackViewConfig {
ManualStackView.Config(
axis: .vertical,
alignment: .center,
spacing: 12,
layoutMargins: .init(hMargin: mutualGroupsPadding, vMargin: 24.0)
)
}
private static let measurementKey_outerStack = "CVComponentThreadDetails.measurementKey_outerStack"
private static let measurementKey_innerStack = "CVComponentThreadDetails.measurementKey_innerStack"
public func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
owsAssertDebug(maxWidth > 0)
var innerSubviewInfos = [ManualStackSubviewInfo]()
let maxContentWidth = maxWidth - (outerStackConfig.layoutMargins.totalWidth +
innerStackConfig.layoutMargins.totalWidth)
innerSubviewInfos.append(avatarSizeClass.size.asManualSubviewInfo)
innerSubviewInfos.append(CGSize(square: 1).asManualSubviewInfo)
let titleSize = CVText.measureLabel(config: titleLabelConfig, maxWidth: maxContentWidth)
innerSubviewInfos.append(titleSize.asManualSubviewInfo)
if let bioText = self.bioText {
let bioSize = CVText.measureLabel(config: bioLabelConfig(text: bioText),
maxWidth: maxContentWidth)
innerSubviewInfos.append(CGSize(square: vSpacingSubtitle).asManualSubviewInfo)
innerSubviewInfos.append(bioSize.asManualSubviewInfo)
}
if let detailsText = self.detailsText {
let detailsSize = CVText.measureLabel(config: detailsLabelConfig(text: detailsText),
maxWidth: maxContentWidth)
innerSubviewInfos.append(CGSize(square: vSpacingSubtitle).asManualSubviewInfo)
innerSubviewInfos.append(detailsSize.asManualSubviewInfo)
}
if let groupDescriptionText = self.groupDescriptionText {
var groupDescriptionSize = CVText.measureLabel(
config: groupDescriptionTextLabelConfig(text: groupDescriptionText),
maxWidth: maxContentWidth
)
groupDescriptionSize.width = maxContentWidth
innerSubviewInfos.append(CGSize(square: vSpacingMutualGroups).asManualSubviewInfo)
innerSubviewInfos.append(groupDescriptionSize.asManualSubviewInfo(hasFixedWidth: true))
}
if let mutualGroupsText = self.mutualGroupsText {
innerSubviewInfos.append(CGSize(square: vSpacingMutualGroups).asManualSubviewInfo)
let mutualGroupsSize: CGSize
if conversationStyle.hasWallpaper {
innerSubviewInfos.append(CGSize(width: maxContentWidth - 16, height: 1).asManualSubviewInfo)
}
let maxGroupWidth = maxContentWidth - mutualGroupsPadding * 2
var groupInfoSubviewInfos = [ManualStackSubviewInfo]()
let groupLabelSize = CVText.measureLabel(
config: mutualGroupsLabelConfig(attributedText: mutualGroupsText),
maxWidth: maxGroupWidth
)
groupInfoSubviewInfos.append(groupLabelSize.asManualSubviewInfo)
if self.shouldShowSafetyTip {
let safetyTipSize = CVText.measureLabel(
config: safetyTipsConfig(),
maxWidth: maxGroupWidth
)
groupInfoSubviewInfos.append(safetyTipSize.asManualSubviewInfo)
}
mutualGroupsSize = ManualStackView.measure(
config: groupStackConfig,
subviewInfos: groupInfoSubviewInfos
).measuredSize
innerSubviewInfos.append(mutualGroupsSize.asManualSubviewInfo)
}
let innerStackMeasurement = ManualStackView.measure(config: innerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_innerStack,
subviewInfos: innerSubviewInfos)
let outerSubviewInfos = [ innerStackMeasurement.measuredSize.asManualSubviewInfo ]
let outerStackMeasurement = ManualStackView.measure(config: outerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_outerStack,
subviewInfos: outerSubviewInfos,
maxWidth: maxWidth)
return outerStackMeasurement.measuredSize
}
// MARK: - Events
public override func handleTap(
sender: UIGestureRecognizer,
componentDelegate: CVComponentDelegate,
componentView: CVComponentView,
renderItem: CVRenderItem
) -> Bool {
guard let componentView = componentView as? CVComponentViewThreadDetails else {
owsFailDebug("Unexpected componentView.")
return false
}
if
canTapTitle,
let contactThread = thread as? TSContactThread,
componentView.titleButton.bounds.contains(sender.location(in: componentView.titleButton))
{
componentDelegate.didTapContactName(thread: contactThread)
return true
}
if
shouldShowSafetyTip,
componentView.showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
{
didShowTips()
return true
}
if threadDetails.isAvatarBlurred {
guard let avatarView = componentView.avatarView else {
owsFailDebug("Missing avatarView.")
return false
}
let location = sender.location(in: avatarView)
if avatarView.bounds.contains(location) {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
if let contactThread = self.thread as? TSContactThread {
SSKEnvironment.shared.contactManagerImplRef.doNotBlurContactAvatar(address: contactThread.contactAddress,
transaction: transaction)
} else if let groupThread = self.thread as? TSGroupThread {
SSKEnvironment.shared.contactManagerImplRef.doNotBlurGroupAvatar(groupThread: groupThread,
transaction: transaction)
} else {
owsFailDebug("Invalid thread.")
}
}
return true
}
}
return false
}
// MARK: -
// Used for rendering some portion of an Conversation View item.
// It could be the entire item or some part thereof.
public class CVComponentViewThreadDetails: NSObject, CVComponentView {
fileprivate var avatarView: ConversationAvatarView?
fileprivate let titleLabel = CVLabel()
fileprivate let titleButton = CVButton()
fileprivate let bioLabel = CVLabel()
fileprivate let detailsLabel = CVLabel()
fileprivate let mutualGroupsLabel = CVLabel()
fileprivate let showTipsButton = OWSRoundedButton()
fileprivate let groupDescriptionPreviewView = GroupDescriptionPreviewView(
shouldDeactivateConstraints: true
)
fileprivate let outerStackView = ManualStackView(name: "Thread details outer")
fileprivate let innerStackView = ManualStackView(name: "Thread details inner")
fileprivate var wallpaperBlurView: CVWallpaperBlurView?
fileprivate func ensureWallpaperBlurView() -> CVWallpaperBlurView {
if let wallpaperBlurView = self.wallpaperBlurView {
return wallpaperBlurView
}
let wallpaperBlurView = CVWallpaperBlurView()
self.wallpaperBlurView = wallpaperBlurView
return wallpaperBlurView
}
public var isDedicatedCellView = false
public var rootView: UIView {
outerStackView
}
// MARK: -
public func setIsCellVisible(_ isCellVisible: Bool) {}
public func reset() {
outerStackView.reset()
innerStackView.reset()
titleLabel.text = nil
titleButton.reset()
bioLabel.text = nil
detailsLabel.text = nil
mutualGroupsLabel.text = nil
groupDescriptionPreviewView.descriptionText = nil
avatarView = nil
wallpaperBlurView?.removeFromSuperview()
wallpaperBlurView?.resetContentAndConfiguration()
}
}
}
extension CVComponentThreadDetails {
private func didShowTips() {
let type: SafetyTipsType = {
switch threadDetails.mutualGroupsTapAction {
case .none, .showContactSafetyTip:
return .contact
case .showGroupSafetyTip:
return .group
}
}()
let viewController = SafetyTipsViewController(type: type)
UIApplication.shared.frontmostViewController?.present(viewController, animated: true)
}
private static func buildMutualGroupsGroupString(
from groupThread: TSGroupThread,
tx: SDSAnyReadTransaction
) -> NSAttributedString? {
guard SSKEnvironment.shared.contactManagerImplRef.shouldShowUnknownThreadWarning(
thread: groupThread,
transaction: tx
) else {
return nil
}
return NSAttributedString.composed(of: [
NSAttributedString.with(
image: UIImage(named: "error-circle-20")!,
font: Self.mutualGroupsFont
),
" ",
OWSLocalizedString(
"SYSTEM_MESSAGE_UNKNOWN_THREAD_WARNING_GROUP",
comment: "Indicator warning about an unknown group thread."
)
])
}
private static func buildMutualGroupsContactString(
for displayName: DisplayName,
in contactThread: TSContactThread,
tx: SDSAnyReadTransaction
) -> (NSAttributedString?, Bool) {
var shouldShowSafetyTip = false
guard !contactThread.contactAddress.isLocalAddress else {
// Don't show mutual groups for "Note to Self".
return (nil, shouldShowSafetyTip)
}
let groupThreads = TSGroupThread.groupThreads(with: contactThread.contactAddress, transaction: tx)
let mutualGroupNames = groupThreads.filter { $0.isLocalUserFullMember && $0.shouldThreadBeVisible }.map { $0.groupNameOrDefault }
let formatString: String
var formatArgs: [AttributedFormatArg] = mutualGroupNames.map { name in
return .string(name, attributes: [.font: Self.mutualGroupsFont.semibold()])
}
switch mutualGroupNames.count {
case 0:
guard SSKEnvironment.shared.contactManagerImplRef.shouldShowUnknownThreadWarning(
thread: contactThread,
transaction: tx
) else {
return (nil, shouldShowSafetyTip)
}
formatString = OWSLocalizedString(
"SYSTEM_MESSAGE_UNKNOWN_THREAD_WARNING_CONTACT",
comment: "Indicator warning about an unknown contact thread."
)
shouldShowSafetyTip = true
case 1:
formatString = OWSLocalizedString(
"THREAD_DETAILS_ONE_MUTUAL_GROUP",
comment: "A string indicating a mutual group the user shares with this contact. Embeds {{mutual group name}}"
)
case 2:
formatString = OWSLocalizedString(
"THREAD_DETAILS_TWO_MUTUAL_GROUP",
comment: "A string indicating two mutual groups the user shares with this contact. Embeds {{mutual group name}}"
)
case 3:
formatString = OWSLocalizedString(
"THREAD_DETAILS_THREE_MUTUAL_GROUP",
comment: "A string indicating three mutual groups the user shares with this contact. Embeds {{mutual group name}}"
)
default:
formatString = OWSLocalizedString(
"THREAD_DETAILS_MORE_MUTUAL_GROUP",
comment: "A string indicating two mutual groups the user shares with this contact and that there are more unlisted. Embeds {{mutual group name}}"
)
// For this string, we want to use the first two groups' names
// and add a final format arg for the number of remaining
// groups.
let firstTwoGroups = Array(formatArgs[0..<2])
let remainingGroupsCount = mutualGroupNames.count - firstTwoGroups.count
formatArgs = firstTwoGroups + [.raw(remainingGroupsCount)]
}
let icon: String
if mutualGroupNames.isEmpty {
icon = "error-circle-20"
} else {
icon = "group-resizable"
}
// In order for the phone number to appear in the same box as the
// mutual groups, it needs to be part of the same label.
let phoneNumberString: NSAttributedString = {
if case .phoneNumber = displayName {
return NSAttributedString()
}
let phoneNumber = contactThread.contactAddress.phoneNumber
let formattedPhoneNumber = phoneNumber.map(PhoneNumber.bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber(_:))
guard let formattedPhoneNumber else {
return NSAttributedString()
}
return NSAttributedString.composed(of: [
NSAttributedString.with(image: Theme.iconImage(.contactInfoPhone), font: Self.mutualGroupsFont),
" ",
formattedPhoneNumber,
"\n"
]).styled(
with: .paragraphSpacingAfter(Self.mutualGroupsFont.lineHeight * 0.5),
.alignment(.center)
)
}()
return (NSAttributedString.composed(of: [
phoneNumberString, // Will be empty if unknown
NSAttributedString.with(
image: UIImage(named: icon)!,
font: Self.mutualGroupsFont
),
" ",
NSAttributedString.make(
fromFormat: formatString,
attributedFormatArgs: formatArgs
)
]), shouldShowSafetyTip)
}
}