Change mentions approach

This commit is contained in:
Niels Andriesse 2019-10-11 15:52:56 +11:00
parent 3cd1febbb5
commit 9b47c646fe
12 changed files with 101 additions and 118 deletions

View file

@ -567,7 +567,6 @@
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 */; };
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 */; };
@ -580,8 +579,8 @@
B894D0712339D6F300B4D94D /* DeviceLinkingModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */; };
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; };
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* UserSelectionView.swift */; };
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */; };
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */; };
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; };
@ -1380,7 +1379,6 @@
B825849F2315024B001B41CB /* RSSFeedPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSFeedPoller.swift; sourceTree = "<group>"; };
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>"; };
@ -1391,8 +1389,8 @@
B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModalDelegate.swift; sourceTree = "<group>"; };
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = "<group>"; };
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionView.swift; sourceTree = "<group>"; };
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionViewDelegate.swift; sourceTree = "<group>"; };
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; };
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionViewDelegate.swift; sourceTree = "<group>"; };
B90418E4183E9DD40038554A /* DateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtil.h; sourceTree = "<group>"; };
B90418E5183E9DD40038554A /* DateUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DateUtil.m; sourceTree = "<group>"; };
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
@ -2656,12 +2654,11 @@
B8162F0222891AD600D46544 /* FriendRequestView.swift */,
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
B84664F2234FE4530083A1CD /* Mention.swift */,
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */,
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
);
path = Loki;
sourceTree = "<group>";
@ -3852,7 +3849,7 @@
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */,
B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
@ -3875,7 +3872,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */,
B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
@ -3886,7 +3883,6 @@
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 */,
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,

View file

@ -1,11 +1,11 @@
// MARK: - User Selection View
@objc(LKUserSelectionView)
final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var users: [String] = [] { didSet { tableView.reloadData() } }
@objc(LKMentionCandidateSelectionView)
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
@objc var hasGroupContext = false
@objc var delegate: UserSelectionViewDelegate?
@objc var delegate: MentionCandidateSelectionViewDelegate?
// MARK: Components
@objc lazy var tableView: UITableView = { // TODO: Make this private
@ -37,30 +37,30 @@ final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelega
// MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
return mentionCandidates.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let user = users[indexPath.row]
cell.user = user
let mentionCandidate = mentionCandidates[indexPath.row]
cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext
return cell
}
// MARK: Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = users[indexPath.row]
delegate?.handleUserSelected(user, from: self)
let mentionCandidate = mentionCandidates[indexPath.row]
delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
}
}
// MARK: - Cell
private extension UserSelectionView {
private extension MentionCandidateSelectionView {
final class Cell : UITableViewCell {
var user = "" { didSet { update() } }
var mentionCandidate = Mention(hexEncodedPublicKey: "", displayName: "") { didSet { update() } }
var hasGroupContext = false
// MARK: Components
@ -118,15 +118,10 @@ private extension UserSelectionView {
// MARK: Updating
private func update() {
var displayName: String = ""
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
displayName = transaction.object(forKey: self.user, inCollection: collection) as! String
}
displayNameLabel.text = displayName
let profilePicture = OWSContactAvatarBuilder(signalId: user, colorName: .blue, diameter: 36).build()
displayNameLabel.text = mentionCandidate.displayName
let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
profilePictureImageView.image = profilePicture
let isUserModerator = LokiGroupChatAPI.isUserModerator(user, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer)
let isUserModerator = LokiGroupChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.isHidden = !isUserModerator || !hasGroupContext
}
}

View file

@ -0,0 +1,6 @@
@objc(LKMentionCandidateSelectionViewDelegate)
protocol MentionCandidateSelectionViewDelegate {
func handleMentionCandidateSelected(_ mentionCandidate: Mention, from mentionCandidateSelectionView: MentionCandidateSelectionView)
}

View file

@ -1,6 +0,0 @@
@objc(LKUserSelectionViewDelegate)
protocol UserSelectionViewDelegate {
func handleUserSelected(_ user: String, from userSelectionView: UserSelectionView)
}

View file

@ -11,7 +11,7 @@ public final class MentionUtilities : NSObject {
@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
let knownUserIDs = LokiAPI.userHexEncodedPublicKeyCache[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() {

View file

@ -5,7 +5,8 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class LKUserSelectionView;
@class LKMention;
@class LKMentionCandidateSelectionView;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class SignalAttachment;
@ -29,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView;
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView;
@end
@ -84,11 +85,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)hideInputMethod;
#pragma mark - User Selection View
#pragma mark - Mention Candidate Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread;
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread;
- (void)hideUserSelectionView;
- (void)hideMentionCandidateSelectionView;
@end

View file

@ -51,7 +51,7 @@ const CGFloat kMaxTextViewHeight = 98;
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
QuotedReplyPreviewDelegate,
LinkPreviewViewDraftDelegate,
LKUserSelectionViewDelegate>
LKMentionCandidateSelectionViewDelegate>
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@ -86,8 +86,8 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
@property (nonatomic) BOOL wasLinkPreviewCancelled;
@property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView;
@property (nonatomic) LKUserSelectionView *userSelectionView;
@property (nonatomic) NSLayoutConstraint *userSelectionViewSizeConstraint;
@property (nonatomic) LKMentionCandidateSelectionView *mentionCandidateSelectionView;
@property (nonatomic) NSLayoutConstraint *mentionCandidateSelectionViewSizeConstraint;
@end
@ -223,12 +223,12 @@ const CGFloat kMaxTextViewHeight = 98;
[vStackWrapper setCompressionResistanceHorizontalLow];
// User Selection View
_userSelectionView = [LKUserSelectionView new];
[self addSubview:self.userSelectionView];
[self.userSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.userSelectionView autoPinWidthToSuperview];
self.userSelectionViewSizeConstraint = [self.userSelectionView autoSetDimension:ALDimensionHeight toSize:0];
self.userSelectionView.delegate = self;
_mentionCandidateSelectionView = [LKMentionCandidateSelectionView new];
[self addSubview:self.mentionCandidateSelectionView];
[self.mentionCandidateSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.mentionCandidateSelectionView autoPinWidthToSuperview];
self.mentionCandidateSelectionViewSizeConstraint = [self.mentionCandidateSelectionView autoSetDimension:ALDimensionHeight toSize:0];
self.mentionCandidateSelectionView.delegate = self;
// H Stack
_hStack = [[UIStackView alloc]
@ -240,7 +240,7 @@ const CGFloat kMaxTextViewHeight = 98;
self.hStack.spacing = 8;
[self addSubview:self.hStack];
[self.hStack autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.userSelectionView];
[self.hStack autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mentionCandidateSelectionView];
[self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom];
[self.hStack setContentHuggingHorizontalLow];
[self.hStack setCompressionResistanceHorizontalLow];
@ -1089,28 +1089,29 @@ const CGFloat kMaxTextViewHeight = 98;
self.borderView.hidden = YES;
}
#pragma mark - User Selection View
#pragma mark - Mention Candidate Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread
{
self.userSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
self.userSelectionView.users = users;
self.userSelectionViewSizeConstraint.constant = 6 + MIN(users.count, 4) * 52;
self.mentionCandidateSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
[self setNeedsLayout];
[self layoutIfNeeded];
[self.userSelectionView.tableView setContentOffset:CGPointMake(0, -6)]; // TODO: Workaround for content offset bug
[self.mentionCandidateSelectionView.tableView setContentOffset:CGPointMake(0, -6)]; // TODO: Workaround for content offset bug
}
- (void)hideUserSelectionView
- (void)hideMentionCandidateSelectionView
{
self.userSelectionViewSizeConstraint.constant = 0;
self.mentionCandidateSelectionViewSizeConstraint.constant = 0;
[self setNeedsLayout];
[self layoutIfNeeded];
[self.mentionCandidateSelectionView.tableView setContentOffset:CGPointMake(0, 0)];
}
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView
{
[self.inputToolbarDelegate handleUserSelected:user from:userSelectionView];
[self.inputToolbarDelegate handleMentionCandidateSelected:mentionCandidate from:mentionCandidateSelectionView];
}
@end

View file

@ -533,7 +533,7 @@ typedef enum : NSUInteger {
userInfo:nil
repeats:YES];
[LKAPI populateUserIDCacheIfNeededFor:thread.uniqueId in:nil];
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:thread.uniqueId in:nil];
}
- (void)dealloc
@ -3023,7 +3023,7 @@ typedef enum : NSUInteger {
{
[self tryToSendAttachments:attachments messageText:messageText];
[self.inputToolbar clearTextMessageAnimated:NO];
[self clearMentions];
[self resetMentions];
// we want to already be at the bottom when the user returns, rather than have to watch
// the new message scroll into view.
@ -3785,68 +3785,56 @@ typedef enum : NSUInteger {
BOOL isBackspace = newText.length < self.oldText.length;
if (isBackspace) {
self.currentMentionStartIndex = -1;
[self.inputToolbar hideMentionCandidateSelectionView];
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) {
if (![mention isContainedIn:newText]) {
[self.mentions removeObject:mention];
}
}
} else if (newText.length > 0) {
NSUInteger currentEndIndex = newText.length - 1;
unichar lastCharacter = [newText characterAtIndex:currentEndIndex];
NSUInteger lastCharacterIndex = newText.length - 1;
unichar lastCharacter = [newText characterAtIndex:lastCharacterIndex];
if (lastCharacter == '@') {
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId];
self.currentMentionStartIndex = (NSInteger)currentEndIndex;
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
NSArray<LKMention *> *mentionCandidates = [LKAPI getMentionCandidatesFor:@"" in:self.thread.uniqueId];
self.currentMentionStartIndex = (NSInteger)lastCharacterIndex;
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
} else if ([NSCharacterSet.whitespaceAndNewlineCharacterSet characterIsMember:lastCharacter]) {
self.currentMentionStartIndex = -1;
[self.inputToolbar hideUserSelectionView];
[self.inputToolbar hideMentionCandidateSelectionView];
} else {
if (self.currentMentionStartIndex != -1) {
NSString *query = [newText substringFromIndex:(NSUInteger)self.currentMentionStartIndex + 1]; // + 1 to get rid of the @
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId];
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
NSArray<LKMention *> *mentionCandidates = [LKAPI getMentionCandidatesFor:query in:self.thread.uniqueId];
[self.inputToolbar showMentionCandidateSelectionViewFor:mentionCandidates in:self.thread];
}
}
}
self.oldText = newText;
}
- (void)handleUserSelected:(NSString *)hexEncodedPublicKey from:(LKUserSelectionView *)userSelectionView
- (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView
{
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];
[self.mentions addObject:mentionCandidate];
NSString *oldText = self.inputToolbar.messageText;
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@", displayName]];
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:[NSString stringWithFormat:@"@%@", mentionCandidate.displayName]];
[self.inputToolbar setMessageText:newText animated:NO];
[self.inputToolbar hideUserSelectionView];
self.currentMentionStartIndex = -1;
[self.inputToolbar hideMentionCandidateSelectionView];
self.oldText = newText;
}
- (NSString *)getSendText
{
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;
NSRange range = [result rangeOfString:[NSString stringWithFormat:@"@%@", mention.displayName]];
result = [result stringByReplacingCharactersInRange:range withString:[[NSString alloc] initWithFormat:@"@%@", mention.hexEncodedPublicKey]];
}
return result;
}
- (void)clearMentions
- (void)resetMentions
{
self.oldText = @"";
self.currentMentionStartIndex = -1;
@ -4101,7 +4089,7 @@ typedef enum : NSUInteger {
{
[self tryToSendAttachments:attachments messageText:messageText];
[self.inputToolbar clearTextMessageAnimated:NO];
[self clearMentions];
[self resetMentions];
[self dismissViewControllerAnimated:YES completion:nil];
// We always want to scroll to the bottom of the conversation after the local user
@ -4472,6 +4460,7 @@ typedef enum : NSUInteger {
[BenchManager startEventWithTitle:@"Send Message milestone: toggleDefaultKeyboard completed"
eventId:@"fromSendUntil_toggleDefaultKeyboard"];
[self.inputToolbar hideMentionCandidateSelectionView];
[self tryToSendTextMessage:[self getSendText] updateKeyboardState:YES];
}
@ -4530,7 +4519,7 @@ typedef enum : NSUInteger {
[BenchManager benchWithTitle:@"clearTextMessageAnimated"
block:^{
[self.inputToolbar clearTextMessageAnimated:YES];
[self clearMentions];
[self resetMentions];
}];
[BenchManager completeEventWithEventId:@"fromSendUntil_clearTextMessageAnimated"];

View file

@ -395,7 +395,7 @@ 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
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor: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

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 public static var userIDCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
// MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared()
@ -296,26 +296,25 @@ public final class LokiAPI : NSObject {
}
// MARK: User ID Caching
@objc public static func cache(_ userHexEncodedPublicKey: String, for threadID: String) {
if let cache = userIDCache[threadID] {
var mutableCache = cache
mutableCache.insert(userHexEncodedPublicKey)
userIDCache[threadID] = mutableCache
@objc public static func cache(_ hexEncodedPublicKey: String, for threadID: String) {
if let cache = userHexEncodedPublicKeyCache[threadID] {
userHexEncodedPublicKeyCache[threadID] = cache.union([ hexEncodedPublicKey ])
} else {
userIDCache[threadID] = [ userHexEncodedPublicKey ]
userHexEncodedPublicKeyCache[threadID] = [ hexEncodedPublicKey ]
}
}
@objc public static func getUserIDs(for query: String, in threadID: String) -> [String] {
@objc public static func getMentionCandidates(for query: String, in threadID: String) -> [Mention] {
// Prepare
guard let cache = userIDCache[threadID] else { return [] }
var candidates: [(id: String, displayName: String)] = []
guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
var candidates: [Mention] = []
// Gather candidates
storage.dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
candidates = cache.flatMap { id in
guard let displayName = transaction.object(forKey: id, inCollection: collection) as! String? else { return nil }
return (id: id, displayName: displayName)
candidates = cache.flatMap { hexEncodedPublicKey in
guard let displayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String? else { return nil }
guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName)
}
}
// Sort alphabetically first
@ -329,11 +328,11 @@ public final class LokiAPI : NSObject {
}
}
// Return
return candidates.map { $0.id } // Inefficient to do this and then look up the display name again later, but easy to interface with Obj-C
return candidates
}
@objc public static func populateUserIDCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
guard userIDCache[threadID] == nil else { return }
@objc public static func populateUserHexEncodedPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
guard userHexEncodedPublicKeyCache[threadID] == nil else { return }
var result: Set<String> = []
func populate(in transaction: YapDatabaseReadWriteTransaction) {
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
@ -351,7 +350,7 @@ public final class LokiAPI : NSObject {
}
}
result.insert(userHexEncodedPublicKey)
userIDCache[threadID] = result
userHexEncodedPublicKeyCache[threadID] = result
}
}

View file

@ -1,13 +1,15 @@
@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
@objc public init(hexEncodedPublicKey: String, displayName: String) {
self.hexEncodedPublicKey = hexEncodedPublicKey
self.displayName = displayName
}
@objc public func isContained(in string: String) -> Bool {
return string.contains(displayName)
}
}

View file

@ -1414,7 +1414,7 @@ NS_ASSUME_NONNULL_BEGIN
}
// Loki: Cache the user hex encoded public key (for mentions)
[LKAPI populateUserIDCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
[LKAPI populateUserHexEncodedPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
[self finalizeIncomingMessage:incomingMessage