mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
# Conflicts: # Session/Settings/SettingsViewModel.swift # Session/Shared/Views/SessionProBadge+Utilities.swift # SessionUIKit/Components/LinkPreviewView.swift
194 lines
8.0 KiB
Swift
194 lines
8.0 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
public enum MentionUtilities {
|
|
private static let currentUserCacheKey: String = "Mention.CurrentUser" // stringlint:ignore
|
|
private static let pubkeyRegex: NSRegularExpression = try! NSRegularExpression(pattern: "@[0-9a-fA-F]{66}", options: [])
|
|
|
|
public enum MentionLocation {
|
|
case incomingMessage
|
|
case outgoingMessage
|
|
case incomingQuote
|
|
case outgoingQuote
|
|
case quoteDraft
|
|
case styleFree
|
|
}
|
|
|
|
public static func getMentions(
|
|
in string: String,
|
|
currentUserSessionIds: Set<String>,
|
|
displayNameRetriever: (String, Bool) -> String?
|
|
) -> (String, [(range: NSRange, profileId: String, isCurrentUser: Bool)]) {
|
|
/// In `Localization` we manually insert RTL isolate markers to ensure mixked RTL/LTR strings
|
|
var workingString: String = string
|
|
let hasRLIPrefix: Bool = workingString.hasPrefix("\u{2067}")
|
|
let hasPDISuffix: Bool = workingString.hasSuffix("\u{2069}")
|
|
|
|
if hasRLIPrefix {
|
|
workingString = String(workingString.dropFirst())
|
|
}
|
|
|
|
if hasPDISuffix {
|
|
workingString = String(workingString.dropLast())
|
|
}
|
|
|
|
var string: String = workingString
|
|
var lastMatchEnd: Int = 0
|
|
var mentions: [(range: NSRange, profileId: String, isCurrentUser: Bool)] = []
|
|
|
|
while let match: NSTextCheckingResult = pubkeyRegex.firstMatch(
|
|
in: string,
|
|
options: .withoutAnchoringBounds,
|
|
range: NSRange(location: lastMatchEnd, length: string.utf16.count - lastMatchEnd)
|
|
) {
|
|
guard let range: Range = Range(match.range, in: string) else { break }
|
|
|
|
let sessionId: String = String(string[range].dropFirst()) // Drop the @
|
|
let isCurrentUser: Bool = currentUserSessionIds.contains(sessionId)
|
|
let maybeTargetString: String? = {
|
|
guard !isCurrentUser else { return "you".localized() }
|
|
guard let displayName: String = displayNameRetriever(sessionId, true) else {
|
|
lastMatchEnd = (match.range.location + match.range.length)
|
|
return nil
|
|
}
|
|
|
|
return displayName
|
|
}()
|
|
|
|
guard let targetString: String = maybeTargetString else { continue }
|
|
|
|
string = string.replacingCharacters(in: range, with: "@\(targetString)") // stringlint:ignore
|
|
lastMatchEnd = (match.range.location + targetString.utf16.count)
|
|
|
|
mentions.append((
|
|
// + 1 to include the @
|
|
range: NSRange(location: match.range.location, length: targetString.utf16.count + 1),
|
|
profileId: sessionId,
|
|
isCurrentUser: isCurrentUser
|
|
))
|
|
}
|
|
|
|
/// Need to add the RTL isolate markers back if we had them
|
|
let finalString: String = (string.containsRTL ?
|
|
"\(LocalizationHelper.forceRTLLeading)\(string)\(LocalizationHelper.forceRTLTrailing)" :
|
|
string
|
|
)
|
|
|
|
return (finalString, mentions)
|
|
}
|
|
|
|
public static func highlightMentionsNoAttributes(
|
|
in string: String,
|
|
currentUserSessionIds: Set<String>,
|
|
displayNameRetriever: (String, Bool) -> String?
|
|
) -> String {
|
|
let (string, _) = getMentions(
|
|
in: string,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
displayNameRetriever: displayNameRetriever
|
|
)
|
|
|
|
return string
|
|
}
|
|
|
|
public static func highlightMentions(
|
|
in string: String,
|
|
currentUserSessionIds: Set<String>,
|
|
location: MentionLocation,
|
|
textColor: ThemeValue,
|
|
attributes: [NSAttributedString.Key: Any],
|
|
displayNameRetriever: (String, Bool) -> String?
|
|
) -> ThemedAttributedString {
|
|
let (string, mentions) = getMentions(
|
|
in: string,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
displayNameRetriever: displayNameRetriever
|
|
)
|
|
|
|
let sizeDiff: CGFloat = (Values.smallFontSize / Values.mediumFontSize)
|
|
let result = ThemedAttributedString(string: string, attributes: attributes)
|
|
let mentionFont = UIFont.boldSystemFont(ofSize: Values.smallFontSize)
|
|
// Iterate in reverse so index ranges remain valid while replacing
|
|
for mention in mentions.sorted(by: { $0.range.location > $1.range.location }) {
|
|
if mention.isCurrentUser && location == .incomingMessage {
|
|
// Build the rendered chip image
|
|
let image: UIImage = UIView.image(
|
|
for: .themedKey(
|
|
MentionUtilities.currentUserCacheKey,
|
|
themeBackgroundColor: .primary
|
|
),
|
|
generator: {
|
|
HighlightMentionView(
|
|
mentionText: (result.string as NSString).substring(with: mention.range),
|
|
font: mentionFont,
|
|
themeTextColor: .dynamicForInterfaceStyle(light: textColor, dark: .black),
|
|
themeBackgroundColor: .primary,
|
|
backgroundCornerRadius: (8 * sizeDiff),
|
|
backgroundPadding: (3 * sizeDiff)
|
|
)
|
|
}
|
|
)
|
|
|
|
/// Set the `accessibilityLabel` to ensure it's still visible to accessibility inspectors
|
|
let attachment: NSTextAttachment = NSTextAttachment()
|
|
attachment.accessibilityLabel = (result.string as NSString).substring(with: mention.range)
|
|
|
|
let offsetY: CGFloat = (mentionFont.capHeight - image.size.height) / 2
|
|
attachment.image = image
|
|
attachment.bounds = CGRect(
|
|
x: 0,
|
|
y: offsetY,
|
|
width: image.size.width,
|
|
height: image.size.height
|
|
)
|
|
|
|
let attachmentString = NSMutableAttributedString(attachment: attachment)
|
|
|
|
// Replace the mention text with the image attachment
|
|
result.replaceCharacters(in: mention.range, with: attachmentString)
|
|
|
|
let insertIndex = mention.range.location + attachmentString.length
|
|
if insertIndex < result.length {
|
|
result.addAttribute(.kern, value: (3 * sizeDiff), range: NSRange(location: insertIndex, length: 1))
|
|
}
|
|
continue
|
|
}
|
|
|
|
result.addAttribute(.font, value: mentionFont, range: mention.range)
|
|
|
|
var targetColor: ThemeValue = textColor
|
|
switch location {
|
|
case .incomingMessage:
|
|
targetColor = .dynamicForInterfaceStyle(light: textColor, dark: .primary)
|
|
case .outgoingMessage:
|
|
targetColor = .dynamicForInterfaceStyle(light: textColor, dark: .black)
|
|
case .outgoingQuote:
|
|
targetColor = .dynamicForInterfaceStyle(light: textColor, dark: .black)
|
|
case .incomingQuote:
|
|
targetColor = .dynamicForInterfaceStyle(light: textColor, dark: .primary)
|
|
case .quoteDraft, .styleFree:
|
|
targetColor = .dynamicForInterfaceStyle(light: textColor, dark: textColor)
|
|
}
|
|
|
|
result.addAttribute(.themeForegroundColor, value: targetColor, range: mention.range)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
public extension String {
|
|
func replacingMentions(
|
|
currentUserSessionIds: Set<String>,
|
|
displayNameRetriever: (String, Bool) -> String?
|
|
) -> String {
|
|
return MentionUtilities.highlightMentionsNoAttributes(
|
|
in: self,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
displayNameRetriever: displayNameRetriever
|
|
)
|
|
}
|
|
}
|