Render mentions in previews & refactor

This commit is contained in:
Niels Andriesse 2019-10-11 14:27:31 +11:00
parent bd62ad099d
commit 8344a86412
5 changed files with 69 additions and 86 deletions

View File

@ -568,6 +568,7 @@
B845B4D4230CD09100D759F0 /* GroupChatPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */; };
B846365B22B7418B00AF1514 /* Identicon+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */; };
B84664F3234FE4540083A1CD /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F2234FE4530083A1CD /* Mention.swift */; };
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
@ -1380,6 +1381,7 @@
B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatPoller.swift; sourceTree = "<group>"; };
B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = "<group>"; };
B84664F2234FE4530083A1CD /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
B84664F4235022F30083A1CD /* MentionUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionUtilities.swift; sourceTree = "<group>"; };
B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = "<group>"; };
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
@ -2701,6 +2703,7 @@
children = (
B86BD08323399ACF000F5AE3 /* Modal.swift */,
B885D5F52334A32100EE0D8E /* UIView+Constraint.swift */,
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3882,6 +3885,7 @@
345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
B84664F3234FE4540083A1CD /* Mention.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,

View File

@ -0,0 +1,49 @@
@objc(LKMentionUtilities)
public final class MentionUtilities : NSObject {
override private init() { }
@objc public static func highlightMentions(in string: String, thread: TSThread) -> String {
return highlightMentions(in: string, isOutgoingMessage: false, thread: thread, attributes: [:]).string // isOutgoingMessage and attributes are irrelevant
}
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, thread: TSThread, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
var string = string
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: [])
let knownUserIDs = LokiAPI.userIDCache[thread.uniqueId!] ?? [] // Should always be populated at this point
var mentions: [NSRange] = []
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count))
while let match = outerMatch, thread.isGroupThread() {
let userID = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
let matchEnd: Int
if knownUserIDs.contains(userID) {
var userDisplayName: String?
if userID == OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey {
userDisplayName = OWSProfileManager.shared().localProfileName()
} else {
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
userDisplayName = transaction.object(forKey: userID, inCollection: collection) as! String?
}
}
if let userDisplayName = userDisplayName {
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(userDisplayName)")
mentions.append(NSRange(location: match.range.location, length: userDisplayName.count + 1)) // + 1 to include the @
matchEnd = match.range.location + userDisplayName.count
} else {
matchEnd = match.range.location + match.range.length
}
} else {
matchEnd = match.range.location + match.range.length
}
outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: matchEnd, length: string.count - matchEnd))
}
let result = NSMutableAttributedString(string: string, attributes: attributes)
mentions.forEach { mention in
let color: UIColor = isOutgoingMessage ? .lokiDarkGray() : .lokiGreen()
result.addAttribute(.backgroundColor, value: color, range: mention)
}
return result
}
}

View File

@ -19,8 +19,6 @@
#import "UIColor+OWS.h"
#import <SignalMessaging/UIView+OWS.h>
// TODO: Reduce code duplication around mention detection
NS_ASSUME_NONNULL_BEGIN
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
@ -685,7 +683,7 @@ NS_ASSUME_NONNULL_BEGIN
font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents
thread:self.viewItem.interaction.thread
isOutgoing:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
isOutgoingMessage:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
}
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
@ -695,7 +693,7 @@ NS_ASSUME_NONNULL_BEGIN
font:(UIFont *)font
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
thread:(TSThread *)thread
isOutgoing:(BOOL)isOutgoing
isOutgoingMessage:(BOOL)isOutgoingMessage
{
textView.hidden = NO;
textView.textColor = textColor;
@ -709,57 +707,18 @@ NS_ASSUME_NONNULL_BEGIN
NSString *text = displayableText.displayText;
NSError *error1;
NSRegularExpression *regex1 = [[NSRegularExpression alloc] initWithPattern:@"@[0-9a-fA-F]*" options:0 error:&error1];
OWSAssertDebug(error1 == nil);
NSSet<NSString *> *knownUserIDs = LKAPI.userIDCache[thread.uniqueId];
NSMutableArray<NSValue *> *mentions = [NSMutableArray new];
NSTextCheckingResult *match1 = [regex1 firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)];
if (match1 != nil && thread.isGroupThread) {
while (YES) {
NSString *userID = [[text substringWithRange:match1.range] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
NSUInteger matchEnd;
if ([knownUserIDs containsObject:userID]) {
__block NSString *userDisplayName;
if ([userID isEqual:OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey]) {
userDisplayName = OWSProfileManager.sharedManager.localProfileName;
} else {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSString *collection = [NSString stringWithFormat:@"%@.%llu", LKGroupChatAPI.publicChatServer, LKGroupChatAPI.publicChatServerID];
userDisplayName = [transaction objectForKey:userID inCollection:collection];
}];
}
if (userDisplayName != nil) {
text = [text stringByReplacingCharactersInRange:match1.range withString:[NSString stringWithFormat:@"@%@", userDisplayName]];
[mentions addObject:[NSValue valueWithRange:NSMakeRange(match1.range.location, userDisplayName.length + 1)]];
matchEnd = match1.range.location + userDisplayName.length;
} else {
matchEnd = match1.range.location + match1.range.length;
}
} else {
matchEnd = match1.range.location + match1.range.length;
}
match1 = [regex1 firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(matchEnd, text.length - matchEnd)];
if (match1 == nil) { break; }
}
}
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
for (NSValue *mention in mentions) {
NSRange range = mention.rangeValue;
UIColor *highlightColor = isOutgoing ? UIColor.lokiDarkGray : UIColor.lokiGreen;
[attributedText addAttribute:NSBackgroundColorAttributeName value:highlightColor range:range];
}
NSMutableAttributedString *attributedText = [LKMentionUtilities highlightMentionsIn:text isOutgoingMessage:isOutgoingMessage thread:thread attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }].mutableCopy;
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText];
NSError *error2;
NSRegularExpression *regex2 = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error2];
OWSAssertDebug(error2 == nil);
for (NSTextCheckingResult *match2 in
[regex2 matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
OWSAssertDebug(match2.range.length >= ConversationSearchController.kMinimumSearchTextLength);
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match2.range];
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match2.range];
NSError *error;
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:[NSRegularExpression escapedPatternForString:searchableText] options:NSRegularExpressionCaseInsensitive error:&error];
OWSAssertDebug(error == nil);
for (NSTextCheckingResult *match in
[regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
}
}
@ -1197,40 +1156,9 @@ NS_ASSUME_NONNULL_BEGIN
- (DisplayableText *)getDisplayableQuotedText
{
if (!self.viewItem.hasQuotedText) { return nil; }
NSString *text = self.viewItem.displayableQuotedText.fullText;
NSString *rawText = self.viewItem.displayableQuotedText.fullText;
TSThread *thread = self.viewItem.interaction.thread;
NSError *error;
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:@"@[0-9a-fA-F]*" options:0 error:&error];
OWSAssertDebug(error == nil);
NSSet<NSString *> *knownUserIDs = LKAPI.userIDCache[thread.uniqueId];
NSTextCheckingResult *match = [regex firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)];
if (match != nil && thread.isGroupThread) {
while (YES) {
NSString *userID = [[text substringWithRange:match.range] stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
NSUInteger matchEnd;
if ([knownUserIDs containsObject:userID]) {
__block NSString *userDisplayName;
if ([userID isEqual:OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey]) {
userDisplayName = OWSProfileManager.sharedManager.localProfileName;
} else {
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSString *collection = [NSString stringWithFormat:@"%@.%llu", LKGroupChatAPI.publicChatServer, LKGroupChatAPI.publicChatServerID];
userDisplayName = [transaction objectForKey:userID inCollection:collection];
}];
}
if (userDisplayName != nil) {
text = [text stringByReplacingCharactersInRange:match.range withString:[NSString stringWithFormat:@"@%@", userDisplayName]];
matchEnd = match.range.location + userDisplayName.length;
} else {
matchEnd = match.range.location + match.range.length;
}
} else {
matchEnd = match.range.location + match.range.length;
}
match = [regex firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(matchEnd, text.length - matchEnd)];
if (match == nil) { break; }
}
}
NSString *text = [LKMentionUtilities highlightMentionsIn:rawText thread:thread];
return [DisplayableText displayableText:text];
}

View File

@ -395,6 +395,8 @@ NS_ASSUME_NONNULL_BEGIN
}
NSString *displayableText = thread.lastMessageText;
if (displayableText) {
[LKAPI populateUserIDCacheIfNeededFor:thread.threadRecord.uniqueId in:nil]; // TODO: Terrible place to do this, but okay for now
displayableText = [LKMentionUtilities highlightMentionsIn:displayableText thread:thread.threadRecord];
[snippetText appendAttributedString:[[NSAttributedString alloc]
initWithString:displayableText
attributes:@{

View File

@ -3,7 +3,7 @@ import PromiseKit
@objc(LKAPI)
public final class LokiAPI : NSObject {
private static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
@objc static var userIDCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
@objc public static var userIDCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
// MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared()