Member labels in quote replies
This commit is contained in:
parent
16303ee655
commit
d63dbbb417
@ -24,6 +24,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
let isOutgoing: Bool
|
||||
let isForPreview: Bool
|
||||
let quotedAuthorName: String
|
||||
let memberLabel: String?
|
||||
|
||||
var quotedInteractionIdentifier: InteractionSnapshotIdentifier? {
|
||||
guard let timestamp = quotedReplyModel.originalMessageTimestamp else {
|
||||
@ -46,7 +47,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
private let remotelySourcedContentStack = ManualStackViewWithLayer(name: "remotelySourcedContentStack")
|
||||
|
||||
private let stripeView = UIView()
|
||||
private let quotedAuthorLabel = CVLabel()
|
||||
private var quotedAuthorLabel = UILabel()
|
||||
private let quotedTextLabel = CVLabel()
|
||||
private let quoteContentSourceLabel = CVLabel()
|
||||
private let quoteReactionHeaderLabel = CVLabel()
|
||||
@ -76,6 +77,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
for: quotedReplyModel.originalMessageAuthorAddress,
|
||||
tx: transaction,
|
||||
).resolvedValue(),
|
||||
memberLabel: quotedReplyModel.originalMessageMemberLabel,
|
||||
)
|
||||
}
|
||||
|
||||
@ -102,6 +104,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
for: quotedReplyModel.originalMessageAuthorAddress,
|
||||
tx: transaction,
|
||||
).resolvedValue(),
|
||||
memberLabel: quotedReplyModel.originalMessageMemberLabel,
|
||||
)
|
||||
}
|
||||
|
||||
@ -118,7 +121,14 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
var isOutgoing: Bool { state.isOutgoing }
|
||||
var isIncoming: Bool { !isOutgoing }
|
||||
var isForPreview: Bool { state.isForPreview }
|
||||
var quotedAuthorName: String { state.quotedAuthorName }
|
||||
fileprivate var quotedAuthorName: NSAttributedString {
|
||||
let padding = " " + String(repeating: SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue, count: 2)
|
||||
if let labelString = state.memberLabel {
|
||||
return NSAttributedString(string: state.quotedAuthorName + padding + labelString)
|
||||
} else {
|
||||
return NSAttributedString(string: state.quotedAuthorName)
|
||||
}
|
||||
}
|
||||
|
||||
let stripeThickness: CGFloat = 4
|
||||
var quotedAuthorFont: UIFont { UIFont.dynamicTypeSubheadlineClamped.semibold() }
|
||||
@ -264,7 +274,7 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
if quotedReplyModel.originalMessageAuthorAddress.isLocalAddress {
|
||||
authorName = CommonStrings.you
|
||||
} else {
|
||||
authorName = quotedAuthorName
|
||||
authorName = quotedAuthorName.string
|
||||
}
|
||||
|
||||
let text: String
|
||||
@ -556,8 +566,23 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
|
||||
var innerVStackSubviews = [UIView]()
|
||||
|
||||
let quotedAuthorLabelConfig = configurator.quotedAuthorLabelConfig
|
||||
quotedAuthorLabelConfig.applyForRendering(label: quotedAuthorLabel)
|
||||
if let memberLabel = state.memberLabel {
|
||||
quotedAuthorLabel = CVCapsuleLabel(
|
||||
attributedText: configurator.quotedAuthorName,
|
||||
textColor: configurator.quotedTextColor,
|
||||
font: configurator.quotedAuthorFont,
|
||||
highlightRange: (configurator.quotedAuthorName.string as NSString).range(of: memberLabel, options: .backwards),
|
||||
highlightFont: .dynamicTypeFootnote,
|
||||
axLabelPrefix: nil,
|
||||
isQuotedReply: true,
|
||||
lineBreakMode: .byTruncatingTail,
|
||||
numberOfLines: 1,
|
||||
)
|
||||
} else {
|
||||
let quotedAuthorLabelConfig = configurator.quotedAuthorLabelConfig
|
||||
quotedAuthorLabelConfig.applyForRendering(label: quotedAuthorLabel)
|
||||
}
|
||||
|
||||
innerVStackSubviews.append(quotedAuthorLabel)
|
||||
|
||||
let quotedTextLabelConfig = configurator.quotedTextLabelConfig
|
||||
@ -854,10 +879,19 @@ public class QuotedMessageView: ManualStackViewWithLayer {
|
||||
var innerVStackSubviewInfos = [ManualStackSubviewInfo]()
|
||||
|
||||
let quotedAuthorLabelConfig = configurator.quotedAuthorLabelConfig
|
||||
let quotedAuthorSize = CVText.measureLabel(
|
||||
config: quotedAuthorLabelConfig,
|
||||
maxWidth: maxLabelWidth,
|
||||
)
|
||||
let quotedAuthorSize: CGSize
|
||||
if state.memberLabel != nil {
|
||||
quotedAuthorSize = CVCapsuleLabel.measureLabel(
|
||||
config: quotedAuthorLabelConfig,
|
||||
maxWidth: maxLabelWidth,
|
||||
)
|
||||
} else {
|
||||
quotedAuthorSize = CVText.measureLabel(
|
||||
config: quotedAuthorLabelConfig,
|
||||
maxWidth: maxLabelWidth,
|
||||
)
|
||||
}
|
||||
|
||||
innerVStackSubviewInfos.append(quotedAuthorSize.asManualSubviewInfo)
|
||||
|
||||
let quotedTextLabelConfig = configurator.quotedTextLabelConfig
|
||||
|
||||
@ -59,7 +59,6 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
|
||||
let outerStack = componentView.outerStack
|
||||
let innerStack = componentView.innerStack
|
||||
let label = componentView.label
|
||||
|
||||
outerStack.reset()
|
||||
innerStack.reset()
|
||||
@ -71,7 +70,6 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
innerStack.addSubviewToFillSuperviewEdges(backgroundView)
|
||||
}
|
||||
|
||||
labelConfig.applyForRendering(label: label)
|
||||
if let memberLabel = state.memberLabel {
|
||||
// Finds the first or last occurance of the member label.
|
||||
// Since member label is appended to the end in LTR and beginning in RTL, this
|
||||
@ -80,13 +78,21 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
if !CurrentAppContext().isRTL {
|
||||
options.insert(.backwards)
|
||||
}
|
||||
|
||||
label.highlightRange = (senderName.string as NSString).range(of: memberLabel, options: options)
|
||||
label.highlightFont = .dynamicTypeFootnote
|
||||
componentView.label = CVCapsuleLabel(
|
||||
attributedText: senderName,
|
||||
textColor: senderNameColor,
|
||||
font: UIFont.dynamicTypeFootnote.semibold(),
|
||||
highlightRange: (senderName.string as NSString).range(of: memberLabel, options: options),
|
||||
highlightFont: .dynamicTypeFootnote,
|
||||
axLabelPrefix: nil, // handled separately in CVItemViewState
|
||||
isQuotedReply: false,
|
||||
)
|
||||
} else {
|
||||
labelConfig.applyForRendering(label: componentView.label)
|
||||
}
|
||||
|
||||
var subviews: [UIView] = []
|
||||
subviews.append(label)
|
||||
subviews.append(componentView.label)
|
||||
|
||||
innerStack.configure(
|
||||
config: innerStackConfig,
|
||||
@ -118,18 +124,6 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
)
|
||||
}
|
||||
|
||||
private var memberLabelConfig: CVLabelConfig {
|
||||
let font = UIFont.dynamicTypeFootnote
|
||||
return CVLabelConfig(
|
||||
text: .text(memberLabel ?? ""),
|
||||
displayConfig: .forUnstyledText(font: font, textColor: senderNameColor),
|
||||
font: font,
|
||||
textColor: senderNameColor,
|
||||
numberOfLines: 0,
|
||||
lineBreakMode: .byWordWrapping,
|
||||
)
|
||||
}
|
||||
|
||||
private var outerStackConfig: CVStackViewConfig {
|
||||
CVStackViewConfig(
|
||||
axis: .vertical,
|
||||
@ -165,7 +159,15 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
)
|
||||
|
||||
var subviewInfos: [ManualStackSubviewInfo] = []
|
||||
let labelSize = CVCapsuleLabel.measureLabel(config: labelConfig, maxWidth: maxWidth)
|
||||
let labelSize: CGSize
|
||||
if state.memberLabel != nil {
|
||||
labelSize = CVCapsuleLabel.measureLabel(
|
||||
config: labelConfig,
|
||||
maxWidth: maxWidth,
|
||||
)
|
||||
} else {
|
||||
labelSize = CVText.measureLabel(config: labelConfig, maxWidth: maxWidth)
|
||||
}
|
||||
let labelInfo = labelSize.asManualSubviewInfo
|
||||
subviewInfos.insert(labelInfo, at: 0)
|
||||
|
||||
@ -192,7 +194,7 @@ public class CVComponentSenderName: CVComponentBase, CVComponent {
|
||||
// It could be the entire item or some part thereof.
|
||||
public class CVComponentViewSenderName: NSObject, CVComponentView {
|
||||
|
||||
fileprivate let label = CVCapsuleLabel()
|
||||
fileprivate var label = UILabel()
|
||||
|
||||
fileprivate let outerStack = ManualStackView(name: "CVComponentViewSenderName.outerStack")
|
||||
fileprivate let innerStack = ManualStackView(name: "CVComponentViewSenderName.innerStack")
|
||||
|
||||
@ -1556,7 +1556,18 @@ private extension CVComponentState.Builder {
|
||||
transaction: transaction,
|
||||
)
|
||||
} else if let quotedMessage = message.quotedMessage {
|
||||
return QuotedReplyModel.build(replyMessage: message, quotedMessage: quotedMessage, transaction: transaction)
|
||||
var memberLabel: String?
|
||||
if
|
||||
BuildFlags.MemberLabel.display, let groupThread = thread as? TSGroupThread,
|
||||
let originalMessageAuthor = quotedMessage.authorAddress.aci
|
||||
{
|
||||
memberLabel = groupThread.groupModel.groupMembership.memberLabel(for: originalMessageAuthor)?.labelForRendering()
|
||||
memberLabel = memberLabel?
|
||||
.components(separatedBy: .whitespaces)
|
||||
.joined(separator: SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue)
|
||||
}
|
||||
|
||||
return QuotedReplyModel.build(replyMessage: message, quotedMessage: quotedMessage, memberLabel: memberLabel, transaction: transaction)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -526,15 +526,17 @@ struct ConversationHeaderBuilder {
|
||||
|
||||
mutating func addMemberLabel(label: String, color: UIColor) -> UILabel {
|
||||
subviews.append(UIView.spacer(withHeight: 4))
|
||||
let memberLabelLabel = CVCapsuleLabel()
|
||||
memberLabelLabel.text = label
|
||||
memberLabelLabel.textColor = color
|
||||
memberLabelLabel.numberOfLines = 0
|
||||
memberLabelLabel.highlightRange = NSRange(location: 0, length: (label as NSString).length)
|
||||
memberLabelLabel.highlightFont = .dynamicTypeSubheadlineClamped
|
||||
memberLabelLabel.axLabelPrefix = OWSLocalizedString(
|
||||
"MEMBER_LABEL_AX_PREFIX",
|
||||
comment: "Accessibility prefix for member labels.",
|
||||
let memberLabelLabel = CVCapsuleLabel(
|
||||
attributedText: NSAttributedString(string: label),
|
||||
textColor: color,
|
||||
font: nil,
|
||||
highlightRange: NSRange(location: 0, length: (label as NSString).length),
|
||||
highlightFont: .dynamicTypeSubheadlineClamped,
|
||||
axLabelPrefix: OWSLocalizedString(
|
||||
"MEMBER_LABEL_AX_PREFIX",
|
||||
comment: "Accessibility prefix for member labels.",
|
||||
),
|
||||
isQuotedReply: false,
|
||||
)
|
||||
|
||||
subviews.append(memberLabelLabel)
|
||||
|
||||
@ -11,13 +11,56 @@ import SignalServiceKit
|
||||
* highlightFont allows for the capsule text to be a different font (e.g. bold or not bold) from the rest of the attributed text.
|
||||
*/
|
||||
public class CVCapsuleLabel: UILabel {
|
||||
public var highlightRange: NSRange?
|
||||
public var highlightFont: UIFont?
|
||||
public var axLabelPrefix: String?
|
||||
public let highlightRange: NSRange
|
||||
public let highlightFont: UIFont
|
||||
public let axLabelPrefix: String?
|
||||
public let isQuotedReply: Bool
|
||||
|
||||
private static let horizontalInset: CGFloat = 6
|
||||
private static let verticalInset: CGFloat = 1
|
||||
|
||||
public init(
|
||||
attributedText: NSAttributedString,
|
||||
textColor: UIColor,
|
||||
font: UIFont?,
|
||||
highlightRange: NSRange,
|
||||
highlightFont: UIFont,
|
||||
axLabelPrefix: String?,
|
||||
isQuotedReply: Bool,
|
||||
lineBreakMode: NSLineBreakMode = .byWordWrapping,
|
||||
numberOfLines: Int = 0,
|
||||
) {
|
||||
self.highlightRange = highlightRange
|
||||
self.highlightFont = highlightFont
|
||||
self.axLabelPrefix = axLabelPrefix
|
||||
self.isQuotedReply = isQuotedReply
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.font = font
|
||||
self.attributedText = attributedText
|
||||
self.textColor = textColor
|
||||
self.lineBreakMode = lineBreakMode
|
||||
self.numberOfLines = numberOfLines
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var capsuleColor: UIColor {
|
||||
if Theme.isDarkThemeEnabled {
|
||||
if isQuotedReply {
|
||||
return UIColor.white.withAlphaComponent(0.20)
|
||||
}
|
||||
return textColor.withAlphaComponent(0.25)
|
||||
}
|
||||
if isQuotedReply {
|
||||
return UIColor.white.withAlphaComponent(0.36)
|
||||
}
|
||||
return textColor.withAlphaComponent(0.1)
|
||||
}
|
||||
|
||||
override public func drawText(in rect: CGRect) {
|
||||
guard let text = self.text else {
|
||||
super.drawText(in: rect)
|
||||
@ -29,9 +72,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
attributedString.addAttribute(.foregroundColor, value: self.textColor!, range: text.entireRange)
|
||||
|
||||
// The highlighted text may have different font than the sender name
|
||||
if let highlightFont, let highlightRange {
|
||||
attributedString.addAttribute(.font, value: highlightFont, range: highlightRange)
|
||||
}
|
||||
attributedString.addAttribute(.font, value: highlightFont, range: highlightRange)
|
||||
|
||||
let textStorage = NSTextStorage(attributedString: attributedString)
|
||||
let layoutManager = NSLayoutManager()
|
||||
@ -51,19 +92,16 @@ public class CVCapsuleLabel: UILabel {
|
||||
horizontalInset = Self.horizontalInset
|
||||
}
|
||||
|
||||
var needsLeadingPadding = false
|
||||
if let highlightRange {
|
||||
needsLeadingPadding = highlightRange.location == 0 && highlightRange.length == (text as NSString).length
|
||||
let glyphRange = layoutManager.glyphRange(forCharacterRange: highlightRange, actualCharacterRange: nil)
|
||||
let needsLeadingPadding = highlightRange.location == 0 && highlightRange.length == (text as NSString).length
|
||||
let glyphRange = layoutManager.glyphRange(forCharacterRange: highlightRange, actualCharacterRange: nil)
|
||||
|
||||
let highlightColor = Theme.isDarkThemeEnabled ? textColor.withAlphaComponent(0.25) : textColor.withAlphaComponent(0.1)
|
||||
layoutManager.enumerateEnclosingRects(forGlyphRange: glyphRange, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { rect, _ in
|
||||
let hOffset = needsLeadingPadding ? horizontalInset : 0
|
||||
let roundedRect = rect.offsetBy(dx: hOffset, dy: -1).insetBy(dx: -Self.horizontalInset, dy: -Self.verticalInset)
|
||||
let path = UIBezierPath(roundedRect: roundedRect, cornerRadius: roundedRect.height / 2)
|
||||
highlightColor.setFill()
|
||||
path.fill()
|
||||
}
|
||||
let highlightColor = capsuleColor
|
||||
layoutManager.enumerateEnclosingRects(forGlyphRange: glyphRange, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0), in: textContainer) { rect, _ in
|
||||
let hOffset = needsLeadingPadding ? horizontalInset : 0
|
||||
let roundedRect = rect.offsetBy(dx: hOffset, dy: -1).insetBy(dx: -Self.horizontalInset, dy: -Self.verticalInset)
|
||||
let path = UIBezierPath(roundedRect: roundedRect, cornerRadius: roundedRect.height / 2)
|
||||
highlightColor.setFill()
|
||||
path.fill()
|
||||
}
|
||||
|
||||
let textOrigin = needsLeadingPadding ? CGPoint(x: .zero + horizontalInset, y: .zero) : CGPoint.zero
|
||||
@ -87,8 +125,7 @@ public class CVCapsuleLabel: UILabel {
|
||||
|
||||
func highlightLabelSize() -> CGSize {
|
||||
guard let text = self.text else { return .zero }
|
||||
guard let fontToUse = highlightFont else { return .zero }
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: fontToUse]
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: highlightFont]
|
||||
let size = (text as NSString).size(withAttributes: attributes)
|
||||
return CGSize(
|
||||
width: size.width + Self.horizontalInset * 2,
|
||||
|
||||
@ -234,15 +234,17 @@ public class ContactCellView: ManualStackView {
|
||||
if
|
||||
let memberLabel = configuration.memberLabel
|
||||
{
|
||||
let memberLabelLabel = CVCapsuleLabel()
|
||||
memberLabelLabel.text = memberLabel.label
|
||||
memberLabelLabel.textColor = memberLabel.groupNameColor
|
||||
memberLabelLabel.numberOfLines = 0
|
||||
memberLabelLabel.highlightRange = NSRange(location: 0, length: (memberLabel.label as NSString).length)
|
||||
memberLabelLabel.highlightFont = .dynamicTypeCaption1Clamped
|
||||
memberLabelLabel.axLabelPrefix = OWSLocalizedString(
|
||||
"MEMBER_LABEL_AX_PREFIX",
|
||||
comment: "Accessibility prefix for member labels.",
|
||||
let memberLabelLabel = CVCapsuleLabel(
|
||||
attributedText: NSAttributedString(string: memberLabel.label),
|
||||
textColor: memberLabel.groupNameColor,
|
||||
font: nil,
|
||||
highlightRange: NSRange(location: 0, length: (memberLabel.label as NSString).length),
|
||||
highlightFont: .dynamicTypeCaption1Clamped,
|
||||
axLabelPrefix: OWSLocalizedString(
|
||||
"MEMBER_LABEL_AX_PREFIX",
|
||||
comment: "Accessibility prefix for member labels.",
|
||||
),
|
||||
isQuotedReply: false,
|
||||
)
|
||||
|
||||
textStackSubviews.append(memberLabelLabel)
|
||||
|
||||
@ -17,6 +17,8 @@ public class QuotedReplyModel {
|
||||
/// Address of the original message's author, be it StoryMessage or TSMessage.
|
||||
public let originalMessageAuthorAddress: SignalServiceAddress
|
||||
|
||||
public let originalMessageMemberLabel: String?
|
||||
|
||||
public let isOriginalMessageAuthorLocalUser: Bool
|
||||
|
||||
/// IFF the original's content was a story message, the emoji used
|
||||
@ -248,6 +250,7 @@ public class QuotedReplyModel {
|
||||
return QuotedReplyModel(
|
||||
originalMessageTimestamp: storyMessage.timestamp,
|
||||
originalMessageAuthorAddress: storyMessage.authorAddress,
|
||||
originalMessageMemberLabel: nil,
|
||||
isOriginalMessageAuthorLocalUser: isOriginalAuthorLocalUser,
|
||||
storyReactionEmoji: reactionEmoji,
|
||||
originalContent: originalContent,
|
||||
@ -319,6 +322,7 @@ public class QuotedReplyModel {
|
||||
return QuotedReplyModel(
|
||||
originalMessageTimestamp: storyTimestamp,
|
||||
originalMessageAuthorAddress: SignalServiceAddress(storyAuthorAci),
|
||||
originalMessageMemberLabel: nil,
|
||||
isOriginalMessageAuthorLocalUser: isOriginalMessageAuthorLocalUser,
|
||||
storyReactionEmoji: message.storyReactionEmoji,
|
||||
originalContent: .expiredStory,
|
||||
@ -336,6 +340,7 @@ public class QuotedReplyModel {
|
||||
public static func build(
|
||||
replyMessage message: TSMessage,
|
||||
quotedMessage: TSQuotedMessage,
|
||||
memberLabel: String?,
|
||||
transaction: DBReadTransaction,
|
||||
) -> QuotedReplyModel {
|
||||
func buildQuotedReplyModel(
|
||||
@ -350,6 +355,7 @@ public class QuotedReplyModel {
|
||||
return QuotedReplyModel(
|
||||
originalMessageTimestamp: quotedMessage.timestampValue?.uint64Value,
|
||||
originalMessageAuthorAddress: quotedMessage.authorAddress,
|
||||
originalMessageMemberLabel: isOriginalAuthorLocalUser ? nil : memberLabel,
|
||||
isOriginalMessageAuthorLocalUser: isOriginalAuthorLocalUser,
|
||||
storyReactionEmoji: nil,
|
||||
originalContent: originalContent,
|
||||
@ -458,6 +464,7 @@ public class QuotedReplyModel {
|
||||
private init(
|
||||
originalMessageTimestamp: UInt64?,
|
||||
originalMessageAuthorAddress: SignalServiceAddress,
|
||||
originalMessageMemberLabel: String?,
|
||||
isOriginalMessageAuthorLocalUser: Bool,
|
||||
storyReactionEmoji: String?,
|
||||
originalContent: OriginalContent,
|
||||
@ -465,6 +472,7 @@ public class QuotedReplyModel {
|
||||
) {
|
||||
self.originalMessageTimestamp = originalMessageTimestamp
|
||||
self.originalMessageAuthorAddress = originalMessageAuthorAddress
|
||||
self.originalMessageMemberLabel = originalMessageMemberLabel
|
||||
self.isOriginalMessageAuthorLocalUser = isOriginalMessageAuthorLocalUser
|
||||
self.storyReactionEmoji = storyReactionEmoji
|
||||
self.originalContent = originalContent
|
||||
|
||||
Loading…
Reference in New Issue
Block a user