From 2f39cd45f3bc50d421653dba514d7e5f5d3b3232 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 11 Oct 2019 10:40:32 +1100 Subject: [PATCH] Mention by display name rather than hex encoded public key --- Signal.xcodeproj/project.pbxproj | 4 + Signal/src/Loki/Mention.swift | 13 +++ Signal/src/Loki/UserSelectionView.swift | 2 +- .../ConversationViewController.m | 97 +++++++++++++++---- 4 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 Signal/src/Loki/Mention.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3cc371d00..0bb047337 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -567,6 +567,7 @@ B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.swift */; }; 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 */; }; 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 */; }; @@ -1378,6 +1379,7 @@ B825849F2315024B001B41CB /* RSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSFeedPoller.swift; sourceTree = ""; }; B845B4D3230CD09000D759F0 /* GroupChatPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatPoller.swift; sourceTree = ""; }; B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identicon+ObjC.swift"; sourceTree = ""; }; + B84664F2234FE4530083A1CD /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; B86BD08023399883000F5AE3 /* QRCodeModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeModal.swift; sourceTree = ""; }; B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = ""; }; @@ -2652,6 +2654,7 @@ B8162F0222891AD600D46544 /* FriendRequestView.swift */, B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */, 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */, + B84664F2234FE4530083A1CD /* Mention.swift */, B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */, B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */, B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */, @@ -3879,6 +3882,7 @@ 345BC30C2047030700257B7C /* OWS2FASettingsViewController.m in Sources */, 340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, + B84664F3234FE4540083A1CD /* Mention.swift in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, diff --git a/Signal/src/Loki/Mention.swift b/Signal/src/Loki/Mention.swift new file mode 100644 index 000000000..89e7867cb --- /dev/null +++ b/Signal/src/Loki/Mention.swift @@ -0,0 +1,13 @@ + +@objc(LKMention) +public final class Mention : NSObject { + @objc public let locationInString: UInt + @objc public let hexEncodedPublicKey: String + @objc public let displayName: String + + @objc public init(locationInString: UInt, hexEncodedPublicKey: String, displayName: String) { + self.locationInString = locationInString + self.hexEncodedPublicKey = hexEncodedPublicKey + self.displayName = displayName + } +} diff --git a/Signal/src/Loki/UserSelectionView.swift b/Signal/src/Loki/UserSelectionView.swift index 570f509e8..0bed79350 100644 --- a/Signal/src/Loki/UserSelectionView.swift +++ b/Signal/src/Loki/UserSelectionView.swift @@ -3,7 +3,7 @@ @objc(LKUserSelectionView) final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate { - @objc var users: [String] = [] { didSet { tableView.reloadData(); tableView.contentOffset = CGPoint.zero } } + @objc var users: [String] = [] { didSet { tableView.reloadData() } } @objc var hasGroupContext = false @objc var delegate: UserSelectionViewDelegate? diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index c5cc8e79c..c4028837c 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -213,7 +213,10 @@ typedef enum : NSUInteger { @property (nonatomic) CGFloat extraContentInsetPadding; @property (nonatomic) CGFloat contentInsetBottom; -@property (nonatomic) NSInteger mentionStartIndex; +// Mentions +@property (nonatomic) NSInteger currentMentionStartIndex; +@property (nonatomic) NSMutableArray *mentions; +@property (nonatomic) NSString *oldText; @end @@ -259,7 +262,8 @@ typedef enum : NSUInteger { self.scrollContinuity = kScrollContinuityBottom; - _mentionStartIndex = -1; + _currentMentionStartIndex = -1; + _mentions = [NSMutableArray new]; } #pragma mark - Dependencies @@ -3018,6 +3022,9 @@ typedef enum : NSUInteger { { [self tryToSendAttachments:attachments messageText:messageText]; [self.inputToolbar clearTextMessageAnimated:NO]; + self.oldText = @""; + self.currentMentionStartIndex = -1; + self.mentions = @[].mutableCopy; // we want to already be at the bottom when the user returns, rather than have to watch // the new message scroll into view. @@ -3769,32 +3776,74 @@ typedef enum : NSUInteger { - (void)textViewDidChange:(UITextView *)textView { - if (textView.text.length == 0) { return; } - [self.typingIndicators didStartTypingOutgoingInputInThread:self.thread]; - NSUInteger currentEndIndex = textView.text.length - 1; - unichar lastCharacter = [textView.text characterAtIndex:currentEndIndex]; - if (lastCharacter == '@') { - NSArray *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId]; - self.mentionStartIndex = (NSInteger)currentEndIndex + 1; - [self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread]; - } else if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:lastCharacter]) { - self.mentionStartIndex = -1; - [self.inputToolbar hideUserSelectionView]; - } else { - if (self.mentionStartIndex != -1) { - NSString *query = [textView.text substringFromIndex:(NSUInteger)self.mentionStartIndex]; - NSArray *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId]; + // Prepare + NSString *newText = textView.text; + // Typing indicators + if (newText.length > 0) { + [self.typingIndicators didStartTypingOutgoingInputInThread:self.thread]; + } + // Mentions + BOOL isBackspace = newText.length < self.oldText.length; + if (isBackspace) { + self.currentMentionStartIndex = -1; + for (LKMention *mention in self.mentions) { + BOOL isValid; + if (mention.locationInString > (NSUInteger)MAX((NSInteger)newText.length - 1, 0)) { + isValid = NO; + } else { + isValid = [[newText substringFromIndex:mention.locationInString] hasPrefix:[NSString stringWithFormat:@"@%@", mention.displayName]]; + } + if (!isValid) { + [self.mentions removeObject:mention]; + } + } + } else if (newText.length > 0) { + NSUInteger currentEndIndex = newText.length - 1; + unichar lastCharacter = [newText characterAtIndex:currentEndIndex]; + if (lastCharacter == '@') { + NSArray *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId]; + self.currentMentionStartIndex = (NSInteger)currentEndIndex; [self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread]; + } else if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:lastCharacter]) { + self.currentMentionStartIndex = -1; + [self.inputToolbar hideUserSelectionView]; + } else { + if (self.currentMentionStartIndex != -1) { + NSString *query = [newText substringFromIndex:(NSUInteger)self.currentMentionStartIndex + 1]; // + 1 to get rid of the @ + NSArray *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId]; + [self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread]; + } } } + self.oldText = newText; } -- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView +- (void)handleUserSelected:(NSString *)hexEncodedPublicKey from:(LKUserSelectionView *)userSelectionView { + NSUInteger mentionStartIndex = (NSUInteger)self.currentMentionStartIndex; + __block NSString *displayName; + [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + NSString *collection = [NSString stringWithFormat:@"%@.%llu", LKGroupChatAPI.publicChatServer, LKGroupChatAPI.publicChatServerID]; + displayName = [transaction objectForKey:hexEncodedPublicKey inCollection:collection]; + }]; + LKMention *mention = [[LKMention alloc] initWithLocationInString:mentionStartIndex hexEncodedPublicKey:hexEncodedPublicKey displayName:displayName]; + [self.mentions addObject:mention]; NSString *oldText = self.inputToolbar.messageText; - NSUInteger mentionStartIndex = (NSUInteger)self.mentionStartIndex; - NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:user]; + NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@", displayName]]; [self.inputToolbar setMessageText:newText animated:NO]; + [self.inputToolbar hideUserSelectionView]; +} + +- (NSString *)getMessageBody +{ + NSString *result = self.inputToolbar.messageText; + NSUInteger shift = 0; + for (LKMention *mention in self.mentions) { + NSRange range = NSMakeRange(mention.locationInString + shift, mention.displayName.length + 1); // + 1 to include the @ + shift = shift + mention.hexEncodedPublicKey.length - mention.displayName.length; + result = [result stringByReplacingCharactersInRange:range withString:[[NSString alloc] initWithFormat:@"@%@", mention.hexEncodedPublicKey]]; + } + return result; } - (void)inputTextViewSendMessagePressed @@ -4045,6 +4094,9 @@ typedef enum : NSUInteger { { [self tryToSendAttachments:attachments messageText:messageText]; [self.inputToolbar clearTextMessageAnimated:NO]; + self.oldText = @""; + self.currentMentionStartIndex = -1; + self.mentions = @[].mutableCopy; [self dismissViewControllerAnimated:YES completion:nil]; // We always want to scroll to the bottom of the conversation after the local user @@ -4415,7 +4467,7 @@ typedef enum : NSUInteger { [BenchManager startEventWithTitle:@"Send Message milestone: toggleDefaultKeyboard completed" eventId:@"fromSendUntil_toggleDefaultKeyboard"]; - [self tryToSendTextMessage:self.inputToolbar.messageText updateKeyboardState:YES]; + [self tryToSendTextMessage:[self getMessageBody] updateKeyboardState:YES]; } - (void)tryToSendTextMessage:(NSString *)text updateKeyboardState:(BOOL)updateKeyboardState @@ -4473,6 +4525,9 @@ typedef enum : NSUInteger { [BenchManager benchWithTitle:@"clearTextMessageAnimated" block:^{ [self.inputToolbar clearTextMessageAnimated:YES]; + self.oldText = @""; + self.currentMentionStartIndex = -1; + self.mentions = @[].mutableCopy; }]; [BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"];