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 */; }; B82584A02315024B001B41CB /* RSSFeedPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B825849F2315024B001B41CB /* RSSFeedPoller.swift */; };
B845B4D4230CD09100D759F0 /* GroupChatPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = B845B4D3230CD09000D759F0 /* GroupChatPoller.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 */; }; 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 */; }; B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; }; B86BD08123399883000F5AE3 /* QRCodeModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08023399883000F5AE3 /* QRCodeModal.swift */; };
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.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 */; }; B894D0712339D6F300B4D94D /* DeviceLinkingModalDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0702339D6F300B4D94D /* DeviceLinkingModalDelegate.swift */; };
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; }; B89841E322B7579F00B1BDC6 /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */; };
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* UserSelectionView.swift */; }; B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */; };
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */; }; B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */; };
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; }; B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; }; B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionCandidateSelectionView.swift; sourceTree = "<group>"; };
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionViewDelegate.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>"; }; 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>"; }; 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>"; }; B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
@ -2656,12 +2654,11 @@
B8162F0222891AD600D46544 /* FriendRequestView.swift */, B8162F0222891AD600D46544 /* FriendRequestView.swift */,
B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */, B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */,
24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */, 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */,
B84664F2234FE4530083A1CD /* Mention.swift */,
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */, B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */, B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */, B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */, B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */, B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
); );
path = Loki; path = Loki;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3852,7 +3849,7 @@
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */, B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */, B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
@ -3875,7 +3872,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */, 45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */, 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */, B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */, 340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */, 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
@ -3886,7 +3883,6 @@
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */, 340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
B84664F3234FE4540083A1CD /* Mention.swift in Sources */,
34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.swift in Sources */,
45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */, 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */,
34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */, 34277A5E20751BDC006049F2 /* OWSQuotedMessageView.m in Sources */,

View File

@ -1,11 +1,11 @@
// MARK: - User Selection View // MARK: - User Selection View
@objc(LKUserSelectionView) @objc(LKMentionCandidateSelectionView)
final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate { final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var users: [String] = [] { didSet { tableView.reloadData() } } @objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
@objc var hasGroupContext = false @objc var hasGroupContext = false
@objc var delegate: UserSelectionViewDelegate? @objc var delegate: MentionCandidateSelectionViewDelegate?
// MARK: Components // MARK: Components
@objc lazy var tableView: UITableView = { // TODO: Make this private @objc lazy var tableView: UITableView = { // TODO: Make this private
@ -37,30 +37,30 @@ final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelega
// MARK: Data // MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count return mentionCandidates.count
} }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell
let user = users[indexPath.row] let mentionCandidate = mentionCandidates[indexPath.row]
cell.user = user cell.mentionCandidate = mentionCandidate
cell.hasGroupContext = hasGroupContext cell.hasGroupContext = hasGroupContext
return cell return cell
} }
// MARK: Interaction // MARK: Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = users[indexPath.row] let mentionCandidate = mentionCandidates[indexPath.row]
delegate?.handleUserSelected(user, from: self) delegate?.handleMentionCandidateSelected(mentionCandidate, from: self)
} }
} }
// MARK: - Cell // MARK: - Cell
private extension UserSelectionView { private extension MentionCandidateSelectionView {
final class Cell : UITableViewCell { final class Cell : UITableViewCell {
var user = "" { didSet { update() } } var mentionCandidate = Mention(hexEncodedPublicKey: "", displayName: "") { didSet { update() } }
var hasGroupContext = false var hasGroupContext = false
// MARK: Components // MARK: Components
@ -118,15 +118,10 @@ private extension UserSelectionView {
// MARK: Updating // MARK: Updating
private func update() { private func update() {
var displayName: String = "" displayNameLabel.text = mentionCandidate.displayName
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in let profilePicture = OWSContactAvatarBuilder(signalId: mentionCandidate.hexEncodedPublicKey, colorName: .blue, diameter: 36).build()
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()
profilePictureImageView.image = profilePicture 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 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 { @objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, thread: TSThread, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
var string = string var string = string
let regex = try! NSRegularExpression(pattern: "@[0-9a-fA-F]*", options: []) 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 mentions: [NSRange] = []
var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count)) var outerMatch = regex.firstMatch(in: string, options: .withoutAnchoringBounds, range: NSRange(location: 0, length: string.count))
while let match = outerMatch, thread.isGroupThread() { while let match = outerMatch, thread.isGroupThread() {

View File

@ -5,7 +5,8 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle; @class ConversationStyle;
@class LKUserSelectionView; @class LKMention;
@class LKMentionCandidateSelectionView;
@class OWSLinkPreviewDraft; @class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel; @class OWSQuotedReplyModel;
@class SignalAttachment; @class SignalAttachment;
@ -29,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha; - (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView; - (void)handleMentionCandidateSelected:(LKMention *)mentionCandidate from:(LKMentionCandidateSelectionView *)mentionCandidateSelectionView;
@end @end
@ -84,11 +85,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)hideInputMethod; - (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 @end

View File

@ -51,7 +51,7 @@ const CGFloat kMaxTextViewHeight = 98;
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate, @interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
QuotedReplyPreviewDelegate, QuotedReplyPreviewDelegate,
LinkPreviewViewDraftDelegate, LinkPreviewViewDraftDelegate,
LKUserSelectionViewDelegate> LKMentionCandidateSelectionViewDelegate>
@property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, readonly) ConversationStyle *conversationStyle;
@ -86,8 +86,8 @@ const CGFloat kMaxTextViewHeight = 98;
@property (nonatomic, nullable) InputLinkPreview *inputLinkPreview; @property (nonatomic, nullable) InputLinkPreview *inputLinkPreview;
@property (nonatomic) BOOL wasLinkPreviewCancelled; @property (nonatomic) BOOL wasLinkPreviewCancelled;
@property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView; @property (nonatomic, nullable, weak) LinkPreviewView *linkPreviewView;
@property (nonatomic) LKUserSelectionView *userSelectionView; @property (nonatomic) LKMentionCandidateSelectionView *mentionCandidateSelectionView;
@property (nonatomic) NSLayoutConstraint *userSelectionViewSizeConstraint; @property (nonatomic) NSLayoutConstraint *mentionCandidateSelectionViewSizeConstraint;
@end @end
@ -223,12 +223,12 @@ const CGFloat kMaxTextViewHeight = 98;
[vStackWrapper setCompressionResistanceHorizontalLow]; [vStackWrapper setCompressionResistanceHorizontalLow];
// User Selection View // User Selection View
_userSelectionView = [LKUserSelectionView new]; _mentionCandidateSelectionView = [LKMentionCandidateSelectionView new];
[self addSubview:self.userSelectionView]; [self addSubview:self.mentionCandidateSelectionView];
[self.userSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop]; [self.mentionCandidateSelectionView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.userSelectionView autoPinWidthToSuperview]; [self.mentionCandidateSelectionView autoPinWidthToSuperview];
self.userSelectionViewSizeConstraint = [self.userSelectionView autoSetDimension:ALDimensionHeight toSize:0]; self.mentionCandidateSelectionViewSizeConstraint = [self.mentionCandidateSelectionView autoSetDimension:ALDimensionHeight toSize:0];
self.userSelectionView.delegate = self; self.mentionCandidateSelectionView.delegate = self;
// H Stack // H Stack
_hStack = [[UIStackView alloc] _hStack = [[UIStackView alloc]
@ -240,7 +240,7 @@ const CGFloat kMaxTextViewHeight = 98;
self.hStack.spacing = 8; self.hStack.spacing = 8;
[self addSubview:self.hStack]; [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 autoPinEdgeToSuperviewSafeArea:ALEdgeBottom];
[self.hStack setContentHuggingHorizontalLow]; [self.hStack setContentHuggingHorizontalLow];
[self.hStack setCompressionResistanceHorizontalLow]; [self.hStack setCompressionResistanceHorizontalLow];
@ -1089,28 +1089,29 @@ const CGFloat kMaxTextViewHeight = 98;
self.borderView.hidden = YES; 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.mentionCandidateSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
self.userSelectionView.users = users; self.mentionCandidateSelectionView.mentionCandidates = mentionCandidates;
self.userSelectionViewSizeConstraint.constant = 6 + MIN(users.count, 4) * 52; self.mentionCandidateSelectionViewSizeConstraint.constant = 6 + MIN(mentionCandidates.count, 4) * 52;
[self setNeedsLayout]; [self setNeedsLayout];
[self layoutIfNeeded]; [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 setNeedsLayout];
[self layoutIfNeeded]; [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 @end

View File

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

View File

@ -395,7 +395,7 @@ NS_ASSUME_NONNULL_BEGIN
} }
NSString *displayableText = thread.lastMessageText; NSString *displayableText = thread.lastMessageText;
if (displayableText) { 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]; displayableText = [LKMentionUtilities highlightMentionsIn:displayableText thread:thread.threadRecord];
[snippetText appendAttributedString:[[NSAttributedString alloc] [snippetText appendAttributedString:[[NSAttributedString alloc]
initWithString:displayableText initWithString:displayableText

View File

@ -3,7 +3,7 @@ import PromiseKit
@objc(LKAPI) @objc(LKAPI)
public final class LokiAPI : NSObject { public final class LokiAPI : NSObject {
private static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date 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 // MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared() internal static let storage = OWSPrimaryStorage.shared()
@ -296,26 +296,25 @@ public final class LokiAPI : NSObject {
} }
// MARK: User ID Caching // MARK: User ID Caching
@objc public static func cache(_ userHexEncodedPublicKey: String, for threadID: String) { @objc public static func cache(_ hexEncodedPublicKey: String, for threadID: String) {
if let cache = userIDCache[threadID] { if let cache = userHexEncodedPublicKeyCache[threadID] {
var mutableCache = cache userHexEncodedPublicKeyCache[threadID] = cache.union([ hexEncodedPublicKey ])
mutableCache.insert(userHexEncodedPublicKey)
userIDCache[threadID] = mutableCache
} else { } 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 // Prepare
guard let cache = userIDCache[threadID] else { return [] } guard let cache = userHexEncodedPublicKeyCache[threadID] else { return [] }
var candidates: [(id: String, displayName: String)] = [] var candidates: [Mention] = []
// Gather candidates // Gather candidates
storage.dbReadConnection.read { transaction in storage.dbReadConnection.read { transaction in
let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)" let collection = "\(LokiGroupChatAPI.publicChatServer).\(LokiGroupChatAPI.publicChatServerID)"
candidates = cache.flatMap { id in candidates = cache.flatMap { hexEncodedPublicKey in
guard let displayName = transaction.object(forKey: id, inCollection: collection) as! String? else { return nil } guard let displayName = transaction.object(forKey: hexEncodedPublicKey, inCollection: collection) as! String? else { return nil }
return (id: id, displayName: displayName) guard !displayName.hasPrefix("Anonymous") else { return nil }
return Mention(hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName)
} }
} }
// Sort alphabetically first // Sort alphabetically first
@ -329,11 +328,11 @@ public final class LokiAPI : NSObject {
} }
} }
// Return // 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) { @objc public static func populateUserHexEncodedPublicKeyCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
guard userIDCache[threadID] == nil else { return } guard userHexEncodedPublicKeyCache[threadID] == nil else { return }
var result: Set<String> = [] var result: Set<String> = []
func populate(in transaction: YapDatabaseReadWriteTransaction) { func populate(in transaction: YapDatabaseReadWriteTransaction) {
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return } guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
@ -351,7 +350,7 @@ public final class LokiAPI : NSObject {
} }
} }
result.insert(userHexEncodedPublicKey) result.insert(userHexEncodedPublicKey)
userIDCache[threadID] = result userHexEncodedPublicKeyCache[threadID] = result
} }
} }

View File

@ -1,13 +1,15 @@
@objc(LKMention) @objc(LKMention)
public final class Mention : NSObject { public final class Mention : NSObject {
@objc public let locationInString: UInt
@objc public let hexEncodedPublicKey: String @objc public let hexEncodedPublicKey: String
@objc public let displayName: String @objc public let displayName: String
@objc public init(locationInString: UInt, hexEncodedPublicKey: String, displayName: String) { @objc public init(hexEncodedPublicKey: String, displayName: String) {
self.locationInString = locationInString
self.hexEncodedPublicKey = hexEncodedPublicKey self.hexEncodedPublicKey = hexEncodedPublicKey
self.displayName = displayName 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) // 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]; [LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
[self finalizeIncomingMessage:incomingMessage [self finalizeIncomingMessage:incomingMessage