Message request state updates
This commit is contained in:
parent
90d1aa4310
commit
1a07a9a252
@ -84,6 +84,7 @@
|
||||
04AB61C62E5E37A800405699 /* PollRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C52E5E37A400405699 /* PollRecord.swift */; };
|
||||
04AB61C82E5E399700405699 /* PollOptionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C72E5E399400405699 /* PollOptionRecord.swift */; };
|
||||
04AB61CA2E5E449100405699 /* PollManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C92E5E448A00405699 /* PollManagerTest.swift */; };
|
||||
04AC221D2F86B9F8006CB71A /* NSAttributedStringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AC221C2F86B9F2006CB71A /* NSAttributedStringExtensionTests.swift */; };
|
||||
04BBBE902E259A6900E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */; };
|
||||
04BBBE922E26C92D00E914B1 /* InactivePrimaryDeviceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE912E26C92300E914B1 /* InactivePrimaryDeviceStore.swift */; };
|
||||
04BBBE942E26F00900E914B1 /* InactivePrimaryDeviceStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BBBE932E26F00000E914B1 /* InactivePrimaryDeviceStoreTest.swift */; };
|
||||
@ -4233,6 +4234,7 @@
|
||||
04AB61C52E5E37A400405699 /* PollRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRecord.swift; sourceTree = "<group>"; };
|
||||
04AB61C72E5E399400405699 /* PollOptionRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionRecord.swift; sourceTree = "<group>"; };
|
||||
04AB61C92E5E448A00405699 /* PollManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollManagerTest.swift; sourceTree = "<group>"; };
|
||||
04AC221C2F86B9F2006CB71A /* NSAttributedStringExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedStringExtensionTests.swift; sourceTree = "<group>"; };
|
||||
04B975452E43A4AA00E20364 /* BackupRefreshManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupRefreshManager.swift; sourceTree = "<group>"; };
|
||||
04B975472E43BFE000E20364 /* BackupRefreshManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupRefreshManagerTest.swift; sourceTree = "<group>"; };
|
||||
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePrimaryDeviceReminderMegaphone.swift; sourceTree = "<group>"; };
|
||||
@ -14362,6 +14364,7 @@
|
||||
50D5E2422980B53000899660 /* LinkValidatorTest.swift */,
|
||||
F94261F2289B1B5400460798 /* LRUCacheTest.swift */,
|
||||
F94261FC289B1B5400460798 /* MathOWSTests.swift */,
|
||||
04AC221C2F86B9F2006CB71A /* NSAttributedStringExtensionTests.swift */,
|
||||
F94261E9289B1B5400460798 /* NSData+ImageTest.swift */,
|
||||
D95E149C2E3D22FA00B5B70B /* ObjectRetainerTest.swift */,
|
||||
F96BB60629A528BD001C18DF /* OWS2FAManagerTest.swift */,
|
||||
@ -20238,6 +20241,7 @@
|
||||
50CDC6592DFB92FD00824B4A /* MonitorTest.swift in Sources */,
|
||||
5056B3BF2DEED72800F55320 /* MonotonicDateTest.swift in Sources */,
|
||||
5079F3C22DA47554008430EC /* NotificationPreconditionTest.swift in Sources */,
|
||||
04AC221D2F86B9F8006CB71A /* NSAttributedStringExtensionTests.swift in Sources */,
|
||||
F9426256289B1B5500460798 /* NSData+ImageTest.swift in Sources */,
|
||||
D979CC3A2AD3964E006AAC49 /* Numbers+Random.swift in Sources */,
|
||||
D95E149D2E3D22FD00B5B70B /* ObjectRetainerTest.swift in Sources */,
|
||||
|
||||
@ -552,6 +552,7 @@ public class CVQuotedMessageView: ManualStackViewWithLayer {
|
||||
presentationContext: configurator.isIncoming ? .messageBubbleQuoteReplyIncoming : .messageBubbleQuoteReplyOutgoing,
|
||||
lineBreakMode: .byTruncatingTail,
|
||||
numberOfLines: 0,
|
||||
signalSymbolRange: nil,
|
||||
onTap: nil,
|
||||
)
|
||||
} else {
|
||||
@ -852,6 +853,7 @@ public class CVQuotedMessageView: ManualStackViewWithLayer {
|
||||
highlightFont: .dynamicTypeFootnote,
|
||||
presentationContext: configurator.isIncoming ? .messageBubbleQuoteReplyIncoming : .messageBubbleQuoteReplyOutgoing,
|
||||
maxWidth: maxLabelWidth,
|
||||
signalSymbolRange: nil,
|
||||
)
|
||||
} else {
|
||||
quotedAuthorSize = CVText.measureLabel(
|
||||
|
||||
@ -105,6 +105,7 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
highlightFont: .dynamicTypeFootnote,
|
||||
axLabelPrefix: nil, // handled separately in CVItemViewState
|
||||
presentationContext: .messageBubbleRegular,
|
||||
signalSymbolRange: nil,
|
||||
onTap: nil,
|
||||
)
|
||||
} else {
|
||||
@ -189,6 +190,7 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
highlightFont: .dynamicTypeFootnote,
|
||||
presentationContext: .messageBubbleRegular,
|
||||
maxWidth: maxWidth,
|
||||
signalSymbolRange: nil,
|
||||
)
|
||||
} else {
|
||||
labelSize = CVText.measureLabel(config: labelConfig, maxWidth: maxWidth)
|
||||
|
||||
@ -474,8 +474,6 @@ public struct CVComponentState: Equatable {
|
||||
|
||||
struct ThreadDetails: Equatable {
|
||||
struct SafetySection: Equatable {
|
||||
/// For "⚠️ Review Carefully"
|
||||
let shouldShowLowTrustWarning: Bool
|
||||
/// For "Profile names are not verified"
|
||||
let shouldShowProfileNamesEducation: Bool
|
||||
/// For phone numbers or group member count
|
||||
|
||||
@ -188,7 +188,6 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
innerViews.append(groupDescriptionPreviewView)
|
||||
}
|
||||
|
||||
let namesEducationLabel = componentView.profileNamesEducationLabel
|
||||
let detailsButton = componentView.detailsButton
|
||||
let mutualGroupsLabel = componentView.mutualGroupsLabel
|
||||
let showTipsButton = componentView.showTipsButton
|
||||
@ -198,14 +197,6 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
var groupInfoSubviews: [UIView] = []
|
||||
|
||||
if let safetySection = threadDetails.safetySection {
|
||||
if safetySection.shouldShowLowTrustWarning {
|
||||
let reviewCarefullyLabel = componentView.reviewCarefullyLabel
|
||||
groupInfoSubviews.append(reviewCarefullyLabel)
|
||||
let config = self.reviewCarefullyConfig()
|
||||
config.applyForRendering(label: reviewCarefullyLabel)
|
||||
groupInfoSubviewInfos.append(reviewCarefullyLabel.sizeThatFitsMaxSize.asManualSubviewInfo)
|
||||
}
|
||||
|
||||
innerViews.append(UIView.spacer(withHeight: vSpacingSafetySection(hasWallpaper: conversationStyle.hasWallpaper)))
|
||||
|
||||
if conversationStyle.hasWallpaper {
|
||||
@ -239,14 +230,26 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
- (hPaddingSafetySection * 2)
|
||||
|
||||
if safetySection.shouldShowProfileNamesEducation {
|
||||
let (namesEducationString, symbolRange) = nameNotVerifiedStringAndSymbolRange()
|
||||
let namesEducationLabel = CVCapsuleLabel(
|
||||
attributedText: namesEducationString,
|
||||
textColor: UIColor.Signal.warningLabel,
|
||||
font: .dynamicTypeCallout.medium(),
|
||||
highlightRange: namesEducationString.string.entireRange,
|
||||
highlightFont: .dynamicTypeCallout,
|
||||
axLabelPrefix: nil,
|
||||
presentationContext: .nameNotVerifiedWarning,
|
||||
numberOfLines: 1,
|
||||
signalSymbolRange: symbolRange,
|
||||
onTap: {
|
||||
componentDelegate.didTapNameEducation(type: safetySection.threadType)
|
||||
},
|
||||
)
|
||||
namesEducationLabel.textAlignment = .center
|
||||
groupInfoSubviews.append(namesEducationLabel)
|
||||
let config = namesEducationConfig(type: safetySection.threadType)
|
||||
config.applyForRendering(button: namesEducationLabel)
|
||||
namesEducationLabel.block = { [weak componentDelegate] in
|
||||
componentDelegate?.didTapNameEducation(type: safetySection.threadType)
|
||||
}
|
||||
componentView.profileNamesEducationLabel = namesEducationLabel
|
||||
|
||||
let size = CVText.measureLabel(config: config, maxWidth: maxWidth)
|
||||
let size = namesEducationLabel.labelSize(maxWidth: maxWidth)
|
||||
groupInfoSubviewInfos.append(size.asManualSubviewInfo)
|
||||
}
|
||||
|
||||
@ -398,106 +401,20 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
private static var underlineColor: UIColor { UIColor.Signal.transparentSeparator }
|
||||
|
||||
private static var reviewCarefullyFont: UIFont { .dynamicTypeSubheadline.semibold() }
|
||||
private static var reviewCarefullyTextColor: UIColor { UIColor(rgbHex: 0xA88746) }
|
||||
|
||||
private func reviewCarefullyConfig() -> CVLabelConfig {
|
||||
CVLabelConfig(
|
||||
text: .attributedText(
|
||||
.composed(of: [
|
||||
NSAttributedString.with(
|
||||
image: UIImage(named: "error-triangle-fill-compact")!,
|
||||
font: .dynamicTypeCallout,
|
||||
centerVerticallyRelativeTo: Self.reviewCarefullyFont,
|
||||
heightReference: .pointSize,
|
||||
),
|
||||
SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue,
|
||||
OWSLocalizedString(
|
||||
"SYSTEM_MESSAGE_UNKNOWN_THREAD_REVIEW_CAREFULLY_WARNING",
|
||||
comment: "Indicator warning about an unknown contact thread",
|
||||
),
|
||||
]).styled(with: .alignment(.center)),
|
||||
),
|
||||
displayConfig: .forUnstyledText(
|
||||
font: Self.reviewCarefullyFont,
|
||||
textColor: Self.reviewCarefullyTextColor,
|
||||
),
|
||||
font: Self.reviewCarefullyFont,
|
||||
textColor: Self.reviewCarefullyTextColor,
|
||||
numberOfLines: 0,
|
||||
)
|
||||
}
|
||||
|
||||
private func namesEducationIcon(type: SafetyTipsType) -> UIImage {
|
||||
switch type {
|
||||
case .contact:
|
||||
return UIImage(named: "person-questionmark-compact")!
|
||||
case .group:
|
||||
return UIImage(named: "group-questionmark-compact")!
|
||||
}
|
||||
}
|
||||
|
||||
private func underlinedNamesEducationString(type: SafetyTipsType) -> NSAttributedString {
|
||||
let (subject, predicate): (String, String) = switch type {
|
||||
case .contact:
|
||||
(
|
||||
private func nameNotVerifiedStringAndSymbolRange() -> (NSAttributedString, NSRange) {
|
||||
let symbol = SignalSymbol.personQuestion.attributedString(dynamicTypeBaseSize: UIFont.dynamicTypeCallout.pointSize)
|
||||
let notVerifiedString = NSAttributedString.composed(
|
||||
of: [
|
||||
symbol,
|
||||
SignalSymbol.LeadingCharacter.space.rawValue,
|
||||
OWSLocalizedString(
|
||||
"THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_SUBJECT",
|
||||
comment: "Label displayed below profiles. This is the subject part of the sentence 'Profile names are not verified'. It is embedded into THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_PREDICATE.",
|
||||
comment: "Label displayed below profiles",
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_PREDICATE",
|
||||
comment: "Label displayed below profiles. This is the predicate part of the sentence 'Profile names are not verified'. Embeds {{ THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_SUBJECT }}",
|
||||
),
|
||||
)
|
||||
case .group:
|
||||
(
|
||||
OWSLocalizedString(
|
||||
"THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_SUBJECT",
|
||||
comment: "Label displayed below group info. This is the subject part of the sentence 'Group names are not verified'. It is embedded into THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_PREDICATE.",
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_PREDICATE",
|
||||
comment: "Label displayed below group info. This is the predicate part of the sentence 'Group names are not verified'. Embeds {{ THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_SUBJECT }}",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
let formattedString = String.nonPluralLocalizedStringWithFormat(predicate, subject)
|
||||
let subjectRange = NSString(string: formattedString).range(of: subject)
|
||||
let attributedString = NSMutableAttributedString(string: formattedString)
|
||||
attributedString.addAttributes(
|
||||
[
|
||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
||||
.underlineColor: Self.underlineColor,
|
||||
],
|
||||
range: subjectRange,
|
||||
)
|
||||
return attributedString
|
||||
}
|
||||
|
||||
private func namesEducationConfig(type: SafetyTipsType) -> CVLabelConfig {
|
||||
CVLabelConfig(
|
||||
text: .attributedText(
|
||||
.composed(of: [
|
||||
NSAttributedString.with(
|
||||
image: self.namesEducationIcon(type: type),
|
||||
font: .dynamicTypeCallout,
|
||||
centerVerticallyRelativeTo: Self.mutualGroupsFont,
|
||||
heightReference: .pointSize,
|
||||
),
|
||||
SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue,
|
||||
self.underlinedNamesEducationString(type: type),
|
||||
]).styled(with: .alignment(.center)),
|
||||
),
|
||||
displayConfig: .forUnstyledText(
|
||||
font: Self.mutualGroupsFont,
|
||||
textColor: Self.mutualGroupsTextColor,
|
||||
),
|
||||
font: Self.mutualGroupsFont,
|
||||
textColor: Self.mutualGroupsTextColor,
|
||||
numberOfLines: 0,
|
||||
)
|
||||
return (notVerifiedString, (notVerifiedString.string as NSString).range(of: symbol.string))
|
||||
}
|
||||
|
||||
private func mutualGroupsLabelConfig(attributedText: NSAttributedString) -> CVLabelConfig {
|
||||
@ -754,14 +671,6 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
var groupInfoSubviewInfos = [ManualStackSubviewInfo]()
|
||||
|
||||
if let safetySection = threadDetails.safetySection {
|
||||
if safetySection.shouldShowLowTrustWarning {
|
||||
let reviewCarefullySize = CVText.measureLabel(
|
||||
config: self.reviewCarefullyConfig(),
|
||||
maxWidth: maxGroupWidth,
|
||||
)
|
||||
groupInfoSubviewInfos.append(reviewCarefullySize.asManualSubviewInfo)
|
||||
}
|
||||
|
||||
innerSubviewInfos.append(CGSize(square: vSpacingSafetySection(hasWallpaper: conversationStyle.hasWallpaper)).asManualSubviewInfo)
|
||||
|
||||
let mutualGroupsSize: CGSize
|
||||
@ -770,9 +679,15 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
}
|
||||
|
||||
if safetySection.shouldShowProfileNamesEducation {
|
||||
let size = CVText.measureLabel(
|
||||
config: self.namesEducationConfig(type: safetySection.threadType),
|
||||
maxWidth: maxGroupWidth,
|
||||
let (namesEducationString, symbolRange) = nameNotVerifiedStringAndSymbolRange()
|
||||
let size = CVCapsuleLabel.measureLabel(
|
||||
attributedText: namesEducationString,
|
||||
font: .dynamicTypeCallout.medium(),
|
||||
highlightRange: namesEducationString.string.entireRange,
|
||||
highlightFont: .dynamicTypeCallout,
|
||||
presentationContext: .nonMessageBubble,
|
||||
maxWidth: maxContentWidth,
|
||||
signalSymbolRange: symbolRange,
|
||||
)
|
||||
groupInfoSubviewInfos.append(size.asManualSubviewInfo)
|
||||
}
|
||||
@ -869,7 +784,8 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
|
||||
if
|
||||
safetySection.shouldShowProfileNamesEducation,
|
||||
componentView.profileNamesEducationLabel.bounds.contains(sender.location(in: componentView.profileNamesEducationLabel))
|
||||
let profileNamesEducationLabel = componentView.profileNamesEducationLabel,
|
||||
profileNamesEducationLabel.bounds.contains(sender.location(in: profileNamesEducationLabel))
|
||||
{
|
||||
componentDelegate.didTapNameEducation(type: safetySection.threadType)
|
||||
return true
|
||||
@ -904,8 +820,9 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
fileprivate let titleButton = CVButton()
|
||||
fileprivate let bioLabel = CVLabel()
|
||||
|
||||
fileprivate var profileNamesEducationLabel: CVCapsuleLabel?
|
||||
|
||||
fileprivate let reviewCarefullyLabel = CVLabel()
|
||||
fileprivate let profileNamesEducationLabel = CVButton()
|
||||
fileprivate let detailsButton = CVButton()
|
||||
fileprivate let mutualGroupsLabel = CVLabel()
|
||||
fileprivate let showTipsButton = OWSRoundedButton()
|
||||
@ -944,7 +861,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
|
||||
titleButton.reset()
|
||||
bioLabel.text = nil
|
||||
reviewCarefullyLabel.text = nil
|
||||
profileNamesEducationLabel.reset()
|
||||
profileNamesEducationLabel = nil
|
||||
detailsButton.reset()
|
||||
mutualGroupsLabel.text = nil
|
||||
groupDescriptionPreviewView.descriptionText = nil
|
||||
@ -1085,7 +1002,6 @@ extension CVComponentThreadDetails {
|
||||
let shouldShowUnknownThreadWarning = SSKEnvironment.shared.contactManagerImplRef.isLowTrustGroup(groupThread: groupThread, tx: tx)
|
||||
|
||||
return .init(
|
||||
shouldShowLowTrustWarning: shouldShowUnknownThreadWarning,
|
||||
shouldShowProfileNamesEducation: shouldShowUnknownThreadWarning,
|
||||
detailsText: membersAttributedText,
|
||||
mutualGroupsText: nil,
|
||||
@ -1111,7 +1027,6 @@ extension CVComponentThreadDetails {
|
||||
|
||||
guard !contactThread.isNoteToSelf else {
|
||||
return .init(
|
||||
shouldShowLowTrustWarning: false,
|
||||
shouldShowProfileNamesEducation: false,
|
||||
detailsText: nil,
|
||||
mutualGroupsText: OWSLocalizedString(
|
||||
@ -1131,11 +1046,6 @@ extension CVComponentThreadDetails {
|
||||
|
||||
let isMessageRequest = contactThread.hasPendingMessageRequest(transaction: tx)
|
||||
|
||||
let shouldShowUnknownThreadWarning = SSKEnvironment.shared.contactManagerImplRef.isLowTrustContact(
|
||||
contactThread: contactThread,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
let groupNamesFormatArg: [String] = mutualGroupNames
|
||||
let formattedString: String
|
||||
switch mutualGroupNames.count {
|
||||
@ -1206,17 +1116,19 @@ extension CVComponentThreadDetails {
|
||||
])
|
||||
}()
|
||||
|
||||
let isPhoneContact = phoneNumberString != nil
|
||||
let shouldShowProfileNamesEducation = if isPhoneContact {
|
||||
false
|
||||
let isSystemContact = SSKEnvironment.shared.contactManagerRef.fetchSignalAccount(for: contactThread.contactAddress, transaction: tx) != nil
|
||||
let shouldShowProfileNamesEducation: Bool
|
||||
if isMessageRequest {
|
||||
shouldShowProfileNamesEducation = true
|
||||
} else if case .nickname = displayName {
|
||||
false
|
||||
shouldShowProfileNamesEducation = false
|
||||
} else if isSystemContact {
|
||||
shouldShowProfileNamesEducation = false
|
||||
} else {
|
||||
true
|
||||
shouldShowProfileNamesEducation = true
|
||||
}
|
||||
|
||||
return .init(
|
||||
shouldShowLowTrustWarning: shouldShowUnknownThreadWarning,
|
||||
shouldShowProfileNamesEducation: shouldShowProfileNamesEducation,
|
||||
detailsText: phoneNumberString,
|
||||
mutualGroupsText: NSAttributedString.composed(of: [
|
||||
|
||||
@ -33,6 +33,7 @@ public struct MessageRequestType: Equatable {
|
||||
let isThreadFromHiddenRecipient: Bool
|
||||
let hasReportedSpam: Bool
|
||||
let isLocalUserInvitedMember: Bool
|
||||
let showReviewRequestsCarefullyWarning: Bool
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -94,6 +95,10 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
messageRequestType.hasReportedSpam
|
||||
}
|
||||
|
||||
private var showReviewRequestsCarefullyWarning: Bool {
|
||||
messageRequestType.showReviewRequestsCarefullyWarning
|
||||
}
|
||||
|
||||
weak var delegate: MessageRequestDelegate?
|
||||
|
||||
init(threadViewModel: ThreadViewModel) {
|
||||
@ -170,6 +175,26 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
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,
|
||||
@ -178,6 +203,7 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
isThreadFromHiddenRecipient: isThreadFromHiddenRecipient,
|
||||
hasReportedSpam: hasReportedSpam,
|
||||
isLocalUserInvitedMember: isLocalUserInvitedMember,
|
||||
showReviewRequestsCarefullyWarning: showReviewRequestsCarefullyWarning,
|
||||
)
|
||||
}
|
||||
|
||||
@ -256,10 +282,11 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
)
|
||||
appendLearnMoreLink = true
|
||||
} else {
|
||||
formatString = OWSLocalizedString(
|
||||
"MESSAGE_REQUEST_VIEW_NEW_CONTACT_PROMPT_FORMAT",
|
||||
comment: "A prompt asking if the user wants to accept a conversation invite. Embeds {{contact name}}.",
|
||||
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
|
||||
@ -392,10 +419,15 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
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: Theme.secondaryTextAndIconColor,
|
||||
.paragraphStyle: centered,
|
||||
]),
|
||||
appendLearnMoreLink: false,
|
||||
)
|
||||
@ -452,6 +484,22 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
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: Theme.secondaryTextAndIconColor,
|
||||
.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
|
||||
@ -475,7 +523,10 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
return prepareTextView(attributedString: attributedString, appendLearnMoreLink: appendLearnMoreLink)
|
||||
}
|
||||
|
||||
private func prepareTextView(attributedString: NSAttributedString, appendLearnMoreLink: Bool) -> UITextView {
|
||||
private func prepareTextView(
|
||||
attributedString: NSAttributedString,
|
||||
appendLearnMoreLink: Bool,
|
||||
) -> UITextView {
|
||||
let textView = LinkingTextView()
|
||||
if appendLearnMoreLink {
|
||||
textView.attributedText = .composed(of: [
|
||||
@ -487,6 +538,30 @@ class MessageRequestView: ConversationBottomPanelView {
|
||||
),
|
||||
])
|
||||
.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))
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ class NameEducationSheet: StackSheetViewController {
|
||||
UIColor.Signal.transparentSeparator
|
||||
}
|
||||
|
||||
private static let capsuleColor = UIColor.Signal.warningLabel
|
||||
private let type: SafetyTipsType
|
||||
|
||||
init(type: SafetyTipsType) {
|
||||
@ -28,29 +29,59 @@ class NameEducationSheet: StackSheetViewController {
|
||||
stackView.alignment = .fill
|
||||
stackView.spacing = 12
|
||||
|
||||
stackView.addArrangedSubview(heroImageView)
|
||||
stackView.setCustomSpacing(24, after: heroImageView)
|
||||
stackView.addArrangedSubview(heroImageContainerView)
|
||||
stackView.setCustomSpacing(24, after: heroImageContainerView)
|
||||
stackView.addArrangedSubview(header)
|
||||
stackView.setCustomSpacing(20, after: header)
|
||||
let bulletPoints = self.bulletPoints.map { text in
|
||||
ListPointView(text: text)
|
||||
BulletPointView(text: text)
|
||||
}
|
||||
stackView.addArrangedSubviews(bulletPoints)
|
||||
stackView.setCustomSpacing(20, after: bulletPoints.last!)
|
||||
}
|
||||
|
||||
private lazy var heroImageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.image = switch self.type {
|
||||
private lazy var heroImageContainerView: UIView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.image = switch self.type {
|
||||
case .contact:
|
||||
UIImage(named: "person-questionmark-display")
|
||||
.personQuestionmarkCompact
|
||||
case .group:
|
||||
UIImage(named: "group-questionmark-display")
|
||||
.groupQuestionmarkCompact
|
||||
}
|
||||
view.tintColor = .label
|
||||
view.contentMode = .scaleAspectFit
|
||||
view.autoSetDimension(.height, toSize: 56)
|
||||
return view
|
||||
imageView.tintColor = Self.capsuleColor
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
||||
let imageInnerContainer = UIView()
|
||||
imageInnerContainer.backgroundColor = Self.capsuleColor.withAlphaComponent(0.12)
|
||||
imageInnerContainer.layer.cornerRadius = 24
|
||||
|
||||
imageInnerContainer.addSubview(imageView)
|
||||
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
imageInnerContainer.widthAnchor.constraint(equalToConstant: 68),
|
||||
imageInnerContainer.heightAnchor.constraint(equalToConstant: 48),
|
||||
|
||||
imageView.widthAnchor.constraint(equalToConstant: 32),
|
||||
imageView.heightAnchor.constraint(equalToConstant: 32),
|
||||
imageView.centerXAnchor.constraint(equalTo: imageInnerContainer.centerXAnchor),
|
||||
imageView.centerYAnchor.constraint(equalTo: imageInnerContainer.centerYAnchor),
|
||||
])
|
||||
|
||||
let imageOuterContainer = UIView()
|
||||
imageOuterContainer.addSubview(imageInnerContainer)
|
||||
|
||||
imageInnerContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
imageInnerContainer.centerXAnchor.constraint(equalTo: imageOuterContainer.centerXAnchor),
|
||||
imageInnerContainer.centerYAnchor.constraint(equalTo: imageOuterContainer.centerYAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageOuterContainer.heightAnchor.constraint(equalToConstant: 48),
|
||||
])
|
||||
|
||||
return imageOuterContainer
|
||||
}()
|
||||
|
||||
private lazy var header: UILabel = {
|
||||
@ -93,6 +124,10 @@ class NameEducationSheet: StackSheetViewController {
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_3",
|
||||
comment: "Third bullet point for the explainer sheet for profile names",
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_4",
|
||||
comment: "Fourth bullet point for the explainer sheet for profile names",
|
||||
),
|
||||
]
|
||||
case .group:
|
||||
[
|
||||
@ -108,16 +143,20 @@ class NameEducationSheet: StackSheetViewController {
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_3",
|
||||
comment: "Third bullet point for the explainer sheet for group names",
|
||||
),
|
||||
OWSLocalizedString(
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_4",
|
||||
comment: "Fourth bullet point for the explainer sheet for group names",
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private class ListPointView: UIStackView {
|
||||
private class BulletPointView: UIStackView {
|
||||
init(text: String) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.axis = .horizontal
|
||||
self.alignment = .center
|
||||
self.alignment = .firstBaseline
|
||||
self.spacing = 8
|
||||
|
||||
let label = UILabel()
|
||||
@ -126,17 +165,13 @@ class NameEducationSheet: StackSheetViewController {
|
||||
label.textColor = .label
|
||||
label.font = .dynamicTypeBody
|
||||
|
||||
let bulletPoint = UIView()
|
||||
bulletPoint.backgroundColor = UIColor.Signal.tertiaryLabel
|
||||
let bulletPoint = UILabel()
|
||||
bulletPoint.text = "•"
|
||||
bulletPoint.font = .dynamicTypeBody
|
||||
|
||||
addArrangedSubview(.spacer(withWidth: 4))
|
||||
addArrangedSubview(bulletPoint)
|
||||
addArrangedSubview(label)
|
||||
|
||||
bulletPoint.autoSetDimension(.width, toSize: 4)
|
||||
bulletPoint.autoPinHeightToSuperview(withMargin: 4)
|
||||
bulletPoint.layer.cornerRadius = 2
|
||||
label.setCompressionResistanceHigh()
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
|
||||
@ -593,6 +593,7 @@ struct ConversationHeaderBuilder {
|
||||
),
|
||||
presentationContext: .nonMessageBubble,
|
||||
numberOfLines: 1,
|
||||
signalSymbolRange: nil,
|
||||
onTap: { [weak delegate] in
|
||||
delegate?.didTapMemberLabel()
|
||||
},
|
||||
|
||||
@ -25,6 +25,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -38,6 +39,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
CVCBottomViewType.messageRequestView(
|
||||
@ -49,6 +51,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -62,6 +65,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
CVCBottomViewType.messageRequestView(
|
||||
@ -73,6 +77,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -86,6 +91,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
CVCBottomViewType.messageRequestView(
|
||||
@ -97,6 +103,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -110,6 +117,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
CVCBottomViewType.messageRequestView(
|
||||
@ -121,6 +129,7 @@ class ConversationViewControllerTest: SignalBaseTest {
|
||||
isThreadFromHiddenRecipient: false,
|
||||
hasReportedSpam: false,
|
||||
isLocalUserInvitedMember: false,
|
||||
showReviewRequestsCarefullyWarning: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -4256,10 +4256,13 @@
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_1" = "Be cautious of groups that impersonate organizations and businesses";
|
||||
|
||||
/* Second bullet point for the explainer sheet for group names */
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_2" = "Profile names of members in groups are not verified";
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_2" = "Signal can't verify names and photos";
|
||||
|
||||
/* Third bullet point for the explainer sheet for group names */
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_3" = "Don’t share personal information with people you don’t know";
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_3" = "Signal will never contact you for your registration code, PIN, or recovery key";
|
||||
|
||||
/* Fourth bullet point for the explainer sheet for group names */
|
||||
"GROUP_NAME_EDUCATION_SHEET_BULLET_4" = "Don’t share personal information with people you don’t know";
|
||||
|
||||
/* Header for the explainer sheet for group names */
|
||||
"GROUP_NAME_EDUCATION_SHEET_HEADER_FORMAT" = "<bold>Group names</bold> are chosen by members of the group.";
|
||||
@ -5518,8 +5521,8 @@
|
||||
/* A prompt notifying that the user must share their profile with this group. */
|
||||
"MESSAGE_REQUEST_VIEW_EXISTING_GROUP_PROMPT" = "Continue your chat with this group and share your name and photo with its members?";
|
||||
|
||||
/* A prompt asking if the user wants to accept a conversation invite. Embeds {{contact name}}. */
|
||||
"MESSAGE_REQUEST_VIEW_NEW_CONTACT_PROMPT_FORMAT" = "Let %@ message you and share your name and photo with them? They won’t know you’ve seen their message until you accept.";
|
||||
/* A prompt asking if the user wants to accept a conversation invite. */
|
||||
"MESSAGE_REQUEST_VIEW_NEW_CONTACT_PROMPT" = "Let this person message you and share your name and photo with them? They won’t know you’ve seen their message until you accept.";
|
||||
|
||||
/* A prompt asking if the user wants to accept a group invite. */
|
||||
"MESSAGE_REQUEST_VIEW_NEW_GROUP_PROMPT" = "Join this group and share your name and photo with its members? They won’t know you’ve seen their messages until you accept.";
|
||||
@ -7148,13 +7151,16 @@
|
||||
"PROFILE_NAME_CHANGE_SYSTEM_NONCONTACT_FORMAT" = "%@ changed their profile name to %@.";
|
||||
|
||||
/* First bullet point for the explainer sheet for profile names */
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_1" = "Profile names are not verified";
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_1" = "Signal can't verify names and photos";
|
||||
|
||||
/* Second bullet point for the explainer sheet for profile names */
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_2" = "Be cautious of accounts that impersonate others";
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_2" = "Signal will never contact you for your registration code, PIN, or recovery key";
|
||||
|
||||
/* Third bullet point for the explainer sheet for profile names */
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_3" = "Don’t share personal information with people you don’t know";
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_3" = "Be cautious of accounts that impersonate others";
|
||||
|
||||
/* Fourth bullet point for the explainer sheet for profile names */
|
||||
"PROFILE_NAME_EDUCATION_SHEET_BULLET_4" = "Don’t share personal information with people you don’t know";
|
||||
|
||||
/* Header for the explainer sheet for profile names */
|
||||
"PROFILE_NAME_EDUCATION_SHEET_HEADER_FORMAT" = "<bold>Profile names</bold> on Signal are chosen by their account holder.";
|
||||
@ -9635,7 +9641,7 @@
|
||||
"SYSTEM_MESSAGE_DEFAULT_DISAPPEARING_MESSAGE_TIMER_FORMAT" = "The disappearing message time will be set to %@ when you message them.";
|
||||
|
||||
/* Indicator warning about an unknown contact thread */
|
||||
"SYSTEM_MESSAGE_UNKNOWN_THREAD_REVIEW_CAREFULLY_WARNING" = "Review Carefully";
|
||||
"SYSTEM_MESSAGE_UNKNOWN_THREAD_REVIEW_CAREFULLY_WARNING" = "Review requests carefully";
|
||||
|
||||
/* Tap to Replace Emoji string for reaction configuration */
|
||||
"TAP_REPLACE_EMOJI" = "Tap to replace an emoji";
|
||||
@ -9667,12 +9673,6 @@
|
||||
/* Label for a group you are not in which has four members, listing their names */
|
||||
"THREAD_DETAILS_FOUR_MEMBERS" = "%1$@, %2$@, %3$@, and %4$@";
|
||||
|
||||
/* Label displayed below group info. This is the predicate part of the sentence 'Group names are not verified'. Embeds {{ THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_SUBJECT }} */
|
||||
"THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_PREDICATE" = "%@ are not verified";
|
||||
|
||||
/* Label displayed below group info. This is the subject part of the sentence 'Group names are not verified'. It is embedded into THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_PREDICATE. */
|
||||
"THREAD_DETAILS_GROUP_NAMES_ARE_NOT_VERIFIED_SUBJECT" = "Group names";
|
||||
|
||||
/* Label for a group with more than four members, listing the first three members' names and embedding THREAD_DETAILS_OTHER_MEMBERS_COUNT_%ld as a count of other members */
|
||||
"THREAD_DETAILS_MANY_MEMBERS" = "%1$@, %2$@, %3$@, and %4$@";
|
||||
|
||||
@ -9691,11 +9691,8 @@
|
||||
/* A string indicating a mutual group the user shares with this contact. Embeds {{mutual group name}} */
|
||||
"THREAD_DETAILS_ONE_MUTUAL_GROUP" = "Member of %@";
|
||||
|
||||
/* Label displayed below profiles. This is the predicate part of the sentence 'Profile names are not verified'. Embeds {{ THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_SUBJECT }} */
|
||||
"THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_PREDICATE" = "%@ are not verified";
|
||||
|
||||
/* Label displayed below profiles. This is the subject part of the sentence 'Profile names are not verified'. It is embedded into THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_PREDICATE. */
|
||||
"THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_SUBJECT" = "Profile names";
|
||||
/* Label displayed below profiles */
|
||||
"THREAD_DETAILS_PROFILE_NAMES_ARE_NOT_VERIFIED_SUBJECT" = "Name not verified";
|
||||
|
||||
/* Indicator that a blurred avatar can be revealed by tapping. */
|
||||
"THREAD_DETAILS_TAP_TO_UNBLUR_AVATAR" = "View";
|
||||
|
||||
@ -61,7 +61,6 @@ public class OWSContactsManager: NSObject, ContactsManagerProtocol {
|
||||
|
||||
private let addressesAllowingAvatarDownloadCache = AtomicSet<SignalServiceAddress>(lock: .init())
|
||||
private let groupIdsAllowingAvatarDownloadCache = AtomicSet<Data>(lock: .init())
|
||||
private let addressesNotNeedingLowTrustWarningCache = AtomicSet<SignalServiceAddress>(lock: .init())
|
||||
private let groupIdsNotNeedingLowTrustWarningCache = AtomicSet<Data>(lock: .init())
|
||||
|
||||
private let intersectionQueue = DispatchQueue(label: "org.signal.contacts.intersection")
|
||||
@ -264,38 +263,6 @@ private class SystemContactsCache {
|
||||
|
||||
extension OWSContactsManager: ContactManager {
|
||||
|
||||
// MARK: Low Trust
|
||||
|
||||
public func isLowTrustContact(contactThread: TSContactThread, tx: DBReadTransaction) -> Bool {
|
||||
let address = contactThread.contactAddress
|
||||
|
||||
if addressesNotNeedingLowTrustWarningCache.contains(address) {
|
||||
return false
|
||||
}
|
||||
|
||||
if SSKEnvironment.shared.profileManagerRef.isThread(inProfileWhitelist: contactThread, transaction: tx) {
|
||||
addressesNotNeedingLowTrustWarningCache.insert(address)
|
||||
return false
|
||||
}
|
||||
|
||||
if !contactThread.hasPendingMessageRequest(transaction: tx) {
|
||||
return false
|
||||
}
|
||||
|
||||
if
|
||||
isInWhitelistedGroupsWithLocalUser(
|
||||
otherAddress: address,
|
||||
requireMultipleMutualGroups: true,
|
||||
tx: tx,
|
||||
)
|
||||
{
|
||||
addressesNotNeedingLowTrustWarningCache.insert(address)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
public func isLowTrustGroup(groupThread: TSGroupThread, tx: DBReadTransaction) -> Bool {
|
||||
if groupIdsNotNeedingLowTrustWarningCache.contains(groupThread.groupId) {
|
||||
return false
|
||||
|
||||
@ -219,6 +219,44 @@ public extension NSAttributedString {
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSMutableAttributedString {
|
||||
func applyAttributesToRangeAvoidingSubrange(attributes: [NSAttributedString.Key: Any], range: NSRange, subrangeToAvoid: NSRange) {
|
||||
|
||||
guard NSIntersectionRange(range, subrangeToAvoid).length > 0 else {
|
||||
// subrange does not exist in range, can apply to entire range.
|
||||
for (key, value) in attributes {
|
||||
addAttribute(key, value: value, range: range)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let rangeEnd = NSMaxRange(range)
|
||||
|
||||
let subrangeToAvoidStart = subrangeToAvoid.location
|
||||
let subrangeToAvoidEnd = NSMaxRange(subrangeToAvoid)
|
||||
|
||||
if subrangeToAvoidStart > range.location {
|
||||
let before = NSRange(
|
||||
location: range.location,
|
||||
length: subrangeToAvoidStart - range.location,
|
||||
)
|
||||
for (key, value) in attributes {
|
||||
addAttribute(key, value: value, range: before)
|
||||
}
|
||||
}
|
||||
|
||||
if subrangeToAvoidEnd < rangeEnd {
|
||||
let after = NSRange(
|
||||
location: subrangeToAvoidEnd,
|
||||
length: rangeEnd - subrangeToAvoidEnd,
|
||||
)
|
||||
for (key, value) in attributes {
|
||||
addAttribute(key, value: value, range: after)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unicode isolates help us avoid RTL/LTR formatting issues when substituting
|
||||
/// strings into other strings.
|
||||
///
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import SignalServiceKit
|
||||
|
||||
struct NSAttributedStringExtensionTests {
|
||||
@Test
|
||||
func testAttributeAvoidingSubrange() {
|
||||
var mutableString = NSMutableAttributedString(string: "🐝 kind to all kinds")
|
||||
let attributes = [NSAttributedString.Key.foregroundColor: UIColor.yellow]
|
||||
|
||||
var subrangeToAvoid = (mutableString.string as NSString).range(of: "all")
|
||||
|
||||
mutableString.applyAttributesToRangeAvoidingSubrange(
|
||||
attributes: attributes,
|
||||
range: mutableString.entireRange,
|
||||
subrangeToAvoid: subrangeToAvoid,
|
||||
)
|
||||
|
||||
mutableString.enumerateAttributes(in: mutableString.entireRange, options: []) { attrs, subrange, _ in
|
||||
if NSIntersectionRange(subrange, subrangeToAvoid).length > 0 {
|
||||
#expect(attrs.isEmpty)
|
||||
} else {
|
||||
#expect(attrs[NSAttributedString.Key.foregroundColor] as! NSObject == UIColor.yellow)
|
||||
}
|
||||
}
|
||||
|
||||
// subrange not intersecting range.
|
||||
mutableString = NSMutableAttributedString(string: "🐝 kind to all kinds")
|
||||
subrangeToAvoid = NSRange(location: 25, length: 1)
|
||||
|
||||
mutableString.applyAttributesToRangeAvoidingSubrange(
|
||||
attributes: attributes,
|
||||
range: mutableString.entireRange,
|
||||
subrangeToAvoid: subrangeToAvoid,
|
||||
)
|
||||
|
||||
mutableString.enumerateAttributes(in: mutableString.entireRange, options: []) { attrs, subrange, _ in
|
||||
#expect(attrs[NSAttributedString.Key.foregroundColor] as! NSObject == UIColor.yellow, "Entire string should have attribute")
|
||||
}
|
||||
|
||||
// subrange with partial intersection
|
||||
mutableString = NSMutableAttributedString(string: "🐝 kind to all kinds")
|
||||
subrangeToAvoid = NSRange(location: 18, length: 10)
|
||||
|
||||
mutableString.applyAttributesToRangeAvoidingSubrange(
|
||||
attributes: attributes,
|
||||
range: mutableString.entireRange,
|
||||
subrangeToAvoid: subrangeToAvoid,
|
||||
)
|
||||
|
||||
mutableString.enumerateAttributes(in: mutableString.entireRange, options: []) { attrs, subrange, _ in
|
||||
if NSIntersectionRange(subrange, subrangeToAvoid).length > 0 {
|
||||
#expect(attrs.isEmpty)
|
||||
} else {
|
||||
#expect(attrs[NSAttributedString.Key.foregroundColor] as! NSObject == UIColor.yellow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,6 +38,7 @@ public enum SignalSymbol: Character {
|
||||
case creditcard = "\u{E127}"
|
||||
case edit = "\u{E030}"
|
||||
case error = "\u{E032}"
|
||||
case errorTriangle = "\u{E092}"
|
||||
case file = "\u{E034}"
|
||||
case forward = "\u{E035}"
|
||||
case gif = "\u{E037}"
|
||||
@ -84,6 +85,7 @@ public enum SignalSymbol: Character {
|
||||
case personMinus = "\u{E062}"
|
||||
case personPlus = "\u{E061}"
|
||||
case personX = "\u{E060}"
|
||||
case personQuestion = "\u{E06A}"
|
||||
case phone = "\u{E063}"
|
||||
case phoneFill = "\u{E064}"
|
||||
case photo = "\u{E065}"
|
||||
|
||||
@ -171,6 +171,13 @@ extension UIColor.Signal {
|
||||
)
|
||||
}
|
||||
|
||||
public static var warningLabel: UIColor {
|
||||
UIColor(
|
||||
light: UIColor(rgbHex: 0xB44828),
|
||||
dark: UIColor(rgbHex: 0xEB977D),
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Background
|
||||
|
||||
public static var background: UIColor {
|
||||
@ -530,6 +537,10 @@ extension Color.Signal {
|
||||
Color(UIColor.Signal.emphasisLabel)
|
||||
}
|
||||
|
||||
public static var warningLabel: Color {
|
||||
Color(UIColor.Signal.warningLabel)
|
||||
}
|
||||
|
||||
// MARK: Background
|
||||
|
||||
public static var background: Color {
|
||||
|
||||
@ -18,6 +18,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
case messageBubbleRegular
|
||||
case messageBubbleQuoteReplyIncoming
|
||||
case messageBubbleQuoteReplyOutgoing
|
||||
case nameNotVerifiedWarning
|
||||
}
|
||||
|
||||
public let highlightRange: NSRange
|
||||
@ -43,6 +44,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
presentationContext: PresentationContext,
|
||||
lineBreakMode: NSLineBreakMode = .byTruncatingTail,
|
||||
numberOfLines: Int = 0,
|
||||
signalSymbolRange: NSRange?,
|
||||
onTap: (() -> Void)?,
|
||||
) {
|
||||
self.highlightRange = highlightRange
|
||||
@ -62,11 +64,9 @@ public class CVCapsuleLabel: UILabel {
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapMemberLabel)))
|
||||
|
||||
let attributedString = NSMutableAttributedString(attributedString: attributedText)
|
||||
attributedString.addAttribute(.font, value: self.font!, range: attributedText.entireRange)
|
||||
applyFontToAttributedString(attributedString, signalSymbolRange: signalSymbolRange)
|
||||
attributedString.addAttribute(.foregroundColor, value: textColor, range: attributedText.entireRange)
|
||||
|
||||
// The highlighted text may have different font than the sender name
|
||||
attributedString.addAttribute(.font, value: highlightFont, range: highlightRange)
|
||||
self.attributedText = attributedString
|
||||
}
|
||||
|
||||
@ -88,9 +88,35 @@ public class CVCapsuleLabel: UILabel {
|
||||
return textColor.withAlphaComponent(0.32)
|
||||
}
|
||||
return textColor.withAlphaComponent(0.14)
|
||||
case .nameNotVerifiedWarning:
|
||||
if Theme.isDarkThemeEnabled {
|
||||
return textColor.withAlphaComponent(0.2)
|
||||
}
|
||||
return textColor.withAlphaComponent(0.12)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyFontToAttributedString(_ attributedString: NSMutableAttributedString, signalSymbolRange: NSRange?) {
|
||||
guard let signalSymbolRange else {
|
||||
attributedString.addAttribute(.font, value: self.font!, range: attributedString.entireRange)
|
||||
// The highlighted text may have different font than the sender name
|
||||
attributedString.addAttribute(.font, value: highlightFont, range: highlightRange)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply font avoiding the signal symbol
|
||||
attributedString.applyAttributesToRangeAvoidingSubrange(
|
||||
attributes: [.font: self.font!],
|
||||
range: attributedString.entireRange,
|
||||
subrangeToAvoid: signalSymbolRange,
|
||||
)
|
||||
attributedString.applyAttributesToRangeAvoidingSubrange(
|
||||
attributes: [.font: highlightFont],
|
||||
range: highlightRange,
|
||||
subrangeToAvoid: signalSymbolRange,
|
||||
)
|
||||
}
|
||||
|
||||
@objc
|
||||
func didTapMemberLabel() {
|
||||
onTap?()
|
||||
@ -288,6 +314,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
highlightFont: UIFont,
|
||||
presentationContext: CVCapsuleLabel.PresentationContext,
|
||||
maxWidth: CGFloat,
|
||||
signalSymbolRange: NSRange?,
|
||||
) -> CGSize {
|
||||
let label = CVCapsuleLabel(
|
||||
attributedText: attributedText,
|
||||
@ -297,6 +324,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
highlightFont: highlightFont,
|
||||
axLabelPrefix: nil,
|
||||
presentationContext: presentationContext,
|
||||
signalSymbolRange: signalSymbolRange,
|
||||
onTap: nil,
|
||||
)
|
||||
return label.labelSize(maxWidth: maxWidth)
|
||||
|
||||
@ -246,6 +246,7 @@ public class ContactCellView: ManualStackView {
|
||||
),
|
||||
presentationContext: .nonMessageBubble,
|
||||
numberOfLines: 1,
|
||||
signalSymbolRange: nil,
|
||||
onTap: nil,
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user