mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
588 lines
25 KiB
Swift
588 lines
25 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
import SessionMessagingKit
|
|
|
|
public final class FullConversationCell: UITableViewCell {
|
|
// MARK: - UI
|
|
|
|
private let accentLineView: UIView = UIView()
|
|
|
|
private lazy var profilePictureView: ProfilePictureView = ProfilePictureView()
|
|
|
|
private lazy var displayNameLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
|
result.textColor = Colors.text
|
|
result.lineBreakMode = .byTruncatingTail
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var unreadCountView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity)
|
|
let size = FullConversationCell.unreadCountViewSize
|
|
result.set(.width, greaterThanOrEqualTo: size)
|
|
result.set(.height, to: size)
|
|
result.layer.masksToBounds = true
|
|
result.layer.cornerRadius = (size / 2)
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var unreadCountLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
|
result.textColor = Colors.text
|
|
result.textAlignment = .center
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var hasMentionView: UIView = {
|
|
let result: UIView = UIView()
|
|
result.backgroundColor = Colors.accent
|
|
let size = FullConversationCell.unreadCountViewSize
|
|
result.set(.width, to: size)
|
|
result.set(.height, to: size)
|
|
result.layer.masksToBounds = true
|
|
result.layer.cornerRadius = (size / 2)
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var hasMentionLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = .boldSystemFont(ofSize: Values.verySmallFontSize)
|
|
result.textColor = Colors.text
|
|
result.text = "@"
|
|
result.textAlignment = .center
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var isPinnedIcon: UIImageView = {
|
|
let result: UIImageView = UIImageView(image: UIImage(named: "Pin")?.withRenderingMode(.alwaysTemplate))
|
|
result.contentMode = .scaleAspectFit
|
|
let size = FullConversationCell.unreadCountViewSize
|
|
result.set(.width, to: size)
|
|
result.set(.height, to: size)
|
|
result.tintColor = Colors.pinIcon
|
|
result.layer.masksToBounds = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var timestampLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = .systemFont(ofSize: Values.smallFontSize)
|
|
result.textColor = Colors.text
|
|
result.lineBreakMode = .byTruncatingTail
|
|
result.alpha = Values.lowOpacity
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var snippetLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = .systemFont(ofSize: Values.smallFontSize)
|
|
result.textColor = Colors.text
|
|
result.lineBreakMode = .byTruncatingTail
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var typingIndicatorView = TypingIndicatorView()
|
|
|
|
private lazy var statusIndicatorView: UIImageView = {
|
|
let result: UIImageView = UIImageView()
|
|
result.contentMode = .scaleAspectFit
|
|
result.layer.cornerRadius = (FullConversationCell.statusIndicatorSize / 2)
|
|
result.layer.masksToBounds = true
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var topLabelStackView: UIStackView = {
|
|
let result: UIStackView = UIStackView()
|
|
result.axis = .horizontal
|
|
result.alignment = .center
|
|
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var bottomLabelStackView: UIStackView = {
|
|
let result: UIStackView = UIStackView()
|
|
result.axis = .horizontal
|
|
result.alignment = .center
|
|
result.spacing = Values.smallSpacing / 2 // Effectively Values.smallSpacing because there'll be spacing before and after the invisible spacer
|
|
|
|
return result
|
|
}()
|
|
|
|
// MARK: Settings
|
|
|
|
public static let unreadCountViewSize: CGFloat = 20
|
|
private static let statusIndicatorSize: CGFloat = 14
|
|
|
|
// MARK: - Initialization
|
|
|
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
private func setUpViewHierarchy() {
|
|
let cellHeight: CGFloat = 68
|
|
|
|
// Background color
|
|
backgroundColor = Colors.cellBackground
|
|
|
|
// Highlight color
|
|
let selectedBackgroundView = UIView()
|
|
selectedBackgroundView.backgroundColor = Colors.cellSelected
|
|
self.selectedBackgroundView = selectedBackgroundView
|
|
|
|
// Accent line view
|
|
accentLineView.set(.width, to: Values.accentLineThickness)
|
|
accentLineView.set(.height, to: cellHeight)
|
|
|
|
// Profile picture view
|
|
let profilePictureViewSize = Values.mediumProfilePictureSize
|
|
profilePictureView.set(.width, to: profilePictureViewSize)
|
|
profilePictureView.set(.height, to: profilePictureViewSize)
|
|
profilePictureView.size = profilePictureViewSize
|
|
|
|
// Unread count view
|
|
unreadCountView.addSubview(unreadCountLabel)
|
|
unreadCountLabel.setCompressionResistanceHigh()
|
|
unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView)
|
|
unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4)
|
|
unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4)
|
|
|
|
// Has mention view
|
|
hasMentionView.addSubview(hasMentionLabel)
|
|
hasMentionLabel.setCompressionResistanceHigh()
|
|
hasMentionLabel.pin(to: hasMentionView)
|
|
|
|
// Label stack view
|
|
let topLabelSpacer = UIView.hStretchingSpacer()
|
|
[ displayNameLabel, isPinnedIcon, unreadCountView, hasMentionView, topLabelSpacer, timestampLabel ].forEach{ view in
|
|
topLabelStackView.addArrangedSubview(view)
|
|
}
|
|
|
|
let snippetLabelContainer = UIView()
|
|
snippetLabelContainer.addSubview(snippetLabel)
|
|
snippetLabelContainer.addSubview(typingIndicatorView)
|
|
|
|
let bottomLabelSpacer = UIView.hStretchingSpacer()
|
|
[ snippetLabelContainer, bottomLabelSpacer, statusIndicatorView ].forEach{ view in
|
|
bottomLabelStackView.addArrangedSubview(view)
|
|
}
|
|
|
|
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
|
|
labelContainerView.axis = .vertical
|
|
labelContainerView.alignment = .leading
|
|
labelContainerView.spacing = 6
|
|
labelContainerView.isUserInteractionEnabled = false
|
|
|
|
// Main stack view
|
|
let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, labelContainerView ])
|
|
stackView.axis = .horizontal
|
|
stackView.alignment = .center
|
|
stackView.spacing = Values.mediumSpacing
|
|
contentView.addSubview(stackView)
|
|
|
|
// Constraints
|
|
accentLineView.pin(.top, to: .top, of: contentView)
|
|
accentLineView.pin(.bottom, to: .bottom, of: contentView)
|
|
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
|
|
|
// HACK: The six lines below are part of a workaround for a weird layout bug
|
|
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
|
topLabelStackView.set(.height, to: 20)
|
|
topLabelSpacer.set(.height, to: 20)
|
|
|
|
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
|
bottomLabelStackView.set(.height, to: 18)
|
|
bottomLabelSpacer.set(.height, to: 18)
|
|
|
|
statusIndicatorView.set(.width, to: FullConversationCell.statusIndicatorSize)
|
|
statusIndicatorView.set(.height, to: FullConversationCell.statusIndicatorSize)
|
|
|
|
snippetLabel.pin(to: snippetLabelContainer)
|
|
|
|
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
|
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
|
|
|
stackView.pin(.leading, to: .leading, of: contentView)
|
|
stackView.pin(.top, to: .top, of: contentView)
|
|
|
|
// HACK: The two lines below are part of a workaround for a weird layout bug
|
|
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
|
|
stackView.set(.height, to: cellHeight)
|
|
}
|
|
|
|
// MARK: - Content
|
|
|
|
// MARK: --Search Results
|
|
|
|
public func updateForMessageSearchResult(with cellViewModel: SessionThreadViewModel, searchText: String) {
|
|
profilePictureView.update(
|
|
publicKey: cellViewModel.threadId,
|
|
profile: cellViewModel.profile,
|
|
additionalProfile: cellViewModel.additionalProfile,
|
|
threadVariant: cellViewModel.threadVariant,
|
|
openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
|
|
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
|
)
|
|
|
|
isPinnedIcon.isHidden = true
|
|
unreadCountView.isHidden = true
|
|
hasMentionView.isHidden = true
|
|
displayNameLabel.attributedText = NSMutableAttributedString(
|
|
string: cellViewModel.displayName,
|
|
attributes: [ .foregroundColor: Colors.text]
|
|
)
|
|
timestampLabel.isHidden = false
|
|
timestampLabel.text = cellViewModel.lastInteractionDate.formattedForDisplay
|
|
bottomLabelStackView.isHidden = false
|
|
snippetLabel.attributedText = getHighlightedSnippet(
|
|
content: Interaction.previewText(
|
|
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
|
|
body: cellViewModel.interactionBody,
|
|
authorDisplayName: cellViewModel.authorName(for: .contact),
|
|
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
|
|
attachmentCount: cellViewModel.interactionAttachmentCount,
|
|
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
|
|
),
|
|
authorName: (cellViewModel.authorId != cellViewModel.currentUserPublicKey ?
|
|
cellViewModel.authorName(for: .contact) :
|
|
nil
|
|
),
|
|
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
|
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey,
|
|
searchText: searchText.lowercased(),
|
|
fontSize: Values.smallFontSize
|
|
)
|
|
}
|
|
|
|
public func updateForContactAndGroupSearchResult(with cellViewModel: SessionThreadViewModel, searchText: String) {
|
|
profilePictureView.update(
|
|
publicKey: cellViewModel.threadId,
|
|
profile: cellViewModel.profile,
|
|
additionalProfile: cellViewModel.additionalProfile,
|
|
threadVariant: cellViewModel.threadVariant,
|
|
openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
|
|
useFallbackPicture: (cellViewModel.threadVariant == .openGroup && cellViewModel.openGroupProfilePictureData == nil)
|
|
)
|
|
|
|
isPinnedIcon.isHidden = true
|
|
unreadCountView.isHidden = true
|
|
hasMentionView.isHidden = true
|
|
timestampLabel.isHidden = true
|
|
displayNameLabel.attributedText = getHighlightedSnippet(
|
|
content: cellViewModel.displayName,
|
|
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
|
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey,
|
|
searchText: searchText.lowercased(),
|
|
fontSize: Values.mediumFontSize
|
|
)
|
|
|
|
switch cellViewModel.threadVariant {
|
|
case .contact, .openGroup: bottomLabelStackView.isHidden = true
|
|
|
|
case .closedGroup:
|
|
bottomLabelStackView.isHidden = (cellViewModel.threadMemberNames ?? "").isEmpty
|
|
snippetLabel.attributedText = getHighlightedSnippet(
|
|
content: (cellViewModel.threadMemberNames ?? ""),
|
|
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
|
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey,
|
|
searchText: searchText.lowercased(),
|
|
fontSize: Values.smallFontSize
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: --Standard
|
|
|
|
public func update(with cellViewModel: SessionThreadViewModel) {
|
|
let unreadCount: UInt = (cellViewModel.threadUnreadCount ?? 0)
|
|
backgroundColor = (cellViewModel.threadIsPinned ? Colors.cellPinned : Colors.cellBackground)
|
|
|
|
if cellViewModel.threadIsBlocked == true {
|
|
accentLineView.backgroundColor = Colors.destructive
|
|
accentLineView.alpha = 1
|
|
}
|
|
else {
|
|
accentLineView.backgroundColor = Colors.accent
|
|
accentLineView.alpha = (unreadCount > 0 ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12
|
|
}
|
|
|
|
isPinnedIcon.isHidden = !cellViewModel.threadIsPinned
|
|
unreadCountView.isHidden = (unreadCount <= 0)
|
|
unreadCountLabel.text = (unreadCount < 10000 ? "\(unreadCount)" : "9999+")
|
|
unreadCountLabel.font = .boldSystemFont(
|
|
ofSize: (unreadCount < 10000 ? Values.verySmallFontSize : 8)
|
|
)
|
|
hasMentionView.isHidden = !(
|
|
((cellViewModel.threadUnreadMentionCount ?? 0) > 0) &&
|
|
(cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup)
|
|
)
|
|
profilePictureView.update(
|
|
publicKey: cellViewModel.threadId,
|
|
profile: cellViewModel.profile,
|
|
additionalProfile: cellViewModel.additionalProfile,
|
|
threadVariant: cellViewModel.threadVariant,
|
|
openGroupProfilePictureData: cellViewModel.openGroupProfilePictureData,
|
|
useFallbackPicture: (
|
|
cellViewModel.threadVariant == .openGroup &&
|
|
cellViewModel.openGroupProfilePictureData == nil
|
|
),
|
|
showMultiAvatarForClosedGroup: true
|
|
)
|
|
displayNameLabel.text = cellViewModel.displayName
|
|
timestampLabel.text = cellViewModel.lastInteractionDate.formattedForDisplay
|
|
|
|
if cellViewModel.threadContactIsTyping == true {
|
|
snippetLabel.text = ""
|
|
typingIndicatorView.isHidden = false
|
|
typingIndicatorView.startAnimation()
|
|
}
|
|
else {
|
|
snippetLabel.attributedText = getSnippet(cellViewModel: cellViewModel)
|
|
typingIndicatorView.isHidden = true
|
|
typingIndicatorView.stopAnimation()
|
|
}
|
|
|
|
statusIndicatorView.backgroundColor = nil
|
|
|
|
switch (cellViewModel.interactionVariant, cellViewModel.interactionState) {
|
|
case (.standardOutgoing, .sending):
|
|
statusIndicatorView.image = #imageLiteral(resourceName: "CircleDotDotDot").withRenderingMode(.alwaysTemplate)
|
|
statusIndicatorView.tintColor = Colors.text
|
|
statusIndicatorView.isHidden = false
|
|
|
|
case (.standardOutgoing, .sent):
|
|
statusIndicatorView.image = #imageLiteral(resourceName: "CircleCheck").withRenderingMode(.alwaysTemplate)
|
|
statusIndicatorView.tintColor = Colors.text
|
|
statusIndicatorView.isHidden = false
|
|
|
|
case (.standardOutgoing, .failed):
|
|
statusIndicatorView.image = #imageLiteral(resourceName: "message_status_failed").withRenderingMode(.alwaysTemplate)
|
|
statusIndicatorView.tintColor = Colors.destructive
|
|
statusIndicatorView.isHidden = false
|
|
|
|
default:
|
|
statusIndicatorView.isHidden = true
|
|
}
|
|
}
|
|
|
|
// MARK: - Snippet generation
|
|
|
|
private func getSnippet(cellViewModel: SessionThreadViewModel) -> NSMutableAttributedString {
|
|
// If we don't have an interaction then do nothing
|
|
guard cellViewModel.interactionId != nil else { return NSMutableAttributedString() }
|
|
|
|
let result = NSMutableAttributedString()
|
|
|
|
if Date().timeIntervalSince1970 < (cellViewModel.threadMutedUntilTimestamp ?? 0) {
|
|
result.append(NSAttributedString(
|
|
string: "\u{e067} ",
|
|
attributes: [
|
|
.font: UIFont.ows_elegantIconsFont(10),
|
|
.foregroundColor :Colors.unimportant
|
|
]
|
|
))
|
|
}
|
|
else if cellViewModel.threadOnlyNotifyForMentions == true {
|
|
let imageAttachment = NSTextAttachment()
|
|
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: Colors.unimportant)
|
|
imageAttachment.bounds = CGRect(x: 0, y: -2, width: Values.smallFontSize, height: Values.smallFontSize)
|
|
|
|
let imageString = NSAttributedString(attachment: imageAttachment)
|
|
result.append(imageString)
|
|
result.append(NSAttributedString(
|
|
string: " ",
|
|
attributes: [
|
|
.font: UIFont.ows_elegantIconsFont(10),
|
|
.foregroundColor: Colors.unimportant
|
|
]
|
|
))
|
|
}
|
|
|
|
let font: UIFont = ((cellViewModel.threadUnreadCount ?? 0) > 0 ?
|
|
.boldSystemFont(ofSize: Values.smallFontSize) :
|
|
.systemFont(ofSize: Values.smallFontSize)
|
|
)
|
|
|
|
if cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup {
|
|
let authorName: String = cellViewModel.authorName(for: cellViewModel.threadVariant)
|
|
|
|
result.append(NSAttributedString(
|
|
string: "\(authorName): ",
|
|
attributes: [
|
|
.font: font,
|
|
.foregroundColor: Colors.text
|
|
]
|
|
))
|
|
}
|
|
|
|
result.append(NSAttributedString(
|
|
string: MentionUtilities.highlightMentions(
|
|
in: Interaction.previewText(
|
|
variant: (cellViewModel.interactionVariant ?? .standardIncoming),
|
|
body: cellViewModel.interactionBody,
|
|
threadContactDisplayName: cellViewModel.threadContactName(),
|
|
authorDisplayName: cellViewModel.authorName(for: cellViewModel.threadVariant),
|
|
attachmentDescriptionInfo: cellViewModel.interactionAttachmentDescriptionInfo,
|
|
attachmentCount: cellViewModel.interactionAttachmentCount,
|
|
isOpenGroupInvitation: (cellViewModel.interactionIsOpenGroupInvitation == true)
|
|
),
|
|
threadVariant: cellViewModel.threadVariant,
|
|
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
|
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey
|
|
),
|
|
attributes: [
|
|
.font: font,
|
|
.foregroundColor: Colors.text
|
|
]
|
|
))
|
|
|
|
return result
|
|
}
|
|
|
|
private func getHighlightedSnippet(
|
|
content: String,
|
|
authorName: String? = nil,
|
|
currentUserPublicKey: String,
|
|
currentUserBlindedPublicKey: String?,
|
|
searchText: String,
|
|
fontSize: CGFloat
|
|
) -> NSAttributedString {
|
|
guard !content.isEmpty, content != "NOTE_TO_SELF".localized() else {
|
|
return NSMutableAttributedString(
|
|
string: (authorName != nil && authorName?.isEmpty != true ?
|
|
"\(authorName ?? ""): \(content)" :
|
|
content
|
|
),
|
|
attributes: [ .foregroundColor: Colors.text ]
|
|
)
|
|
}
|
|
|
|
// Replace mentions in the content
|
|
//
|
|
// Note: The 'threadVariant' is used for profile context but in the search results
|
|
// we don't want to include the truncated id as part of the name so we exclude it
|
|
let mentionReplacedContent: String = MentionUtilities.highlightMentions(
|
|
in: content,
|
|
threadVariant: .contact,
|
|
currentUserPublicKey: currentUserPublicKey,
|
|
currentUserBlindedPublicKey: currentUserBlindedPublicKey
|
|
)
|
|
let result: NSMutableAttributedString = NSMutableAttributedString(
|
|
string: mentionReplacedContent,
|
|
attributes: [
|
|
.foregroundColor: Colors.text
|
|
.withAlphaComponent(Values.lowOpacity)
|
|
]
|
|
)
|
|
|
|
// Bold each part of the searh term which matched
|
|
let normalizedSnippet: String = mentionReplacedContent.lowercased()
|
|
var firstMatchRange: Range<String.Index>?
|
|
|
|
SessionThreadViewModel.searchTermParts(searchText)
|
|
.map { part -> String in
|
|
guard part.hasPrefix("\"") && part.hasSuffix("\"") else { return part }
|
|
|
|
return String(part[part.index(after: part.startIndex)..<part.endIndex])
|
|
}
|
|
.forEach { part in
|
|
// Highlight all ranges of the text (Note: The search logic only finds results that start
|
|
// with the term so we use the regex below to ensure we only highlight those cases)
|
|
normalizedSnippet
|
|
.ranges(
|
|
of: (CurrentAppContext().isRTL ?
|
|
"\(part.lowercased())(^|[ ])" :
|
|
"(^|[ ])\(part.lowercased())"
|
|
),
|
|
options: [.regularExpression]
|
|
)
|
|
.forEach { range in
|
|
// Store the range of the first match so we can focus it in the content displayed
|
|
if firstMatchRange == nil {
|
|
firstMatchRange = range
|
|
}
|
|
|
|
let legacyRange: NSRange = NSRange(range, in: normalizedSnippet)
|
|
result.addAttribute(.foregroundColor, value: Colors.text, range: legacyRange)
|
|
result.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: fontSize), range: legacyRange)
|
|
}
|
|
}
|
|
|
|
// We then want to truncate the content so the first matching term is visible
|
|
let startOfSnippet: String.Index = (
|
|
firstMatchRange.map {
|
|
max(
|
|
mentionReplacedContent.startIndex,
|
|
mentionReplacedContent
|
|
.index(
|
|
$0.lowerBound,
|
|
offsetBy: -10,
|
|
limitedBy: mentionReplacedContent.startIndex
|
|
)
|
|
.defaulting(to: mentionReplacedContent.startIndex)
|
|
)
|
|
} ??
|
|
mentionReplacedContent.startIndex
|
|
)
|
|
|
|
// This method determines if the content is probably too long and returns the truncated or untruncated
|
|
// content accordingly
|
|
func truncatingIfNeeded(approxWidth: CGFloat, content: NSAttributedString) -> NSAttributedString {
|
|
let approxFullWidth: CGFloat = (approxWidth + profilePictureView.size + (Values.mediumSpacing * 3))
|
|
|
|
guard ((bounds.width - approxFullWidth) < 0) else { return content }
|
|
|
|
return content.attributedSubstring(
|
|
from: NSRange(startOfSnippet..<normalizedSnippet.endIndex, in: normalizedSnippet)
|
|
)
|
|
}
|
|
|
|
// Now that we have generated the focused snippet add the author name as a prefix (if provided)
|
|
return authorName
|
|
.map { authorName -> NSAttributedString? in
|
|
guard !authorName.isEmpty else { return nil }
|
|
|
|
let authorPrefix: NSAttributedString = NSAttributedString(
|
|
string: "\(authorName): ...",
|
|
attributes: [ .foregroundColor: Colors.text ]
|
|
)
|
|
|
|
return authorPrefix
|
|
.appending(
|
|
truncatingIfNeeded(
|
|
approxWidth: (authorPrefix.size().width + result.size().width),
|
|
content: result
|
|
)
|
|
)
|
|
}
|
|
.defaulting(
|
|
to: truncatingIfNeeded(
|
|
approxWidth: result.size().width,
|
|
content: result
|
|
)
|
|
)
|
|
}
|
|
}
|