Merge commit '21aa2f646465094c82a1d0062aa74f4a482ca31b' into custom-server

# Conflicts:
#	Signal.xcodeproj/project.pbxproj
This commit is contained in:
Mikunj 2019-10-10 09:52:21 +11:00
commit 315ba3ecf9
11 changed files with 361 additions and 31 deletions

View File

@ -578,6 +578,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 */; };
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 */; };
@ -1385,6 +1387,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>"; };
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>"; };
@ -2652,6 +2656,8 @@
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
24BD2608234DA2050008EB0A /* NewPublicChatVC.swift */,
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */,
);
path = Loki;
sourceTree = "<group>";
@ -3839,6 +3845,7 @@
4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */,
34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */,
45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */,
B8B26C8F234D629C004ED98C /* UserSelectionView.swift in Sources */,
34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */,
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */,
B821F2F82272CED3002C88C0 /* DisplayNameVC.swift in Sources */,
@ -3862,6 +3869,7 @@
45FBC5C81DF8575700E9B410 /* CallKitCallManager.swift in Sources */,
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */,
B8B26C91234D8CBD004ED98C /* UserSelectionViewDelegate.swift in Sources */,
340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */,
45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */,
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,

View File

@ -7,7 +7,7 @@
<key>CarthageVersion</key>
<string>0.33.0</string>
<key>OSXVersion</key>
<string>10.14.6</string>
<string>10.15</string>
<key>WebRTCCommit</key>
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
</dict>

View File

@ -0,0 +1,133 @@
// MARK: - User Selection View
@objc(LKUserSelectionView)
final class UserSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
@objc var users: [String] = [] { didSet { tableView.reloadData() } }
@objc var hasGroupContext = false
@objc var delegate: UserSelectionViewDelegate?
// MARK: Components
private lazy var tableView: UITableView = {
let result = UITableView()
result.dataSource = self
result.delegate = self
result.register(Cell.self, forCellReuseIdentifier: "Cell")
result.separatorStyle = .none
result.backgroundColor = .clear
result.contentInset = UIEdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 0)
return result
}()
// MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
addSubview(tableView)
tableView.pin(to: self)
}
// MARK: Data
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.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
cell.hasGroupContext = hasGroupContext
return cell
}
// MARK: Interaction
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let user = users[indexPath.row]
delegate?.handleUserSelected(user, from: self)
}
}
// MARK: - Cell
private extension UserSelectionView {
final class Cell : UITableViewCell {
var user = "" { didSet { update() } }
var hasGroupContext = false
// MARK: Components
private lazy var profilePictureImageView = AvatarImageView()
private lazy var moderatorIconImageView: UIImageView = {
let result = UIImageView(image: #imageLiteral(resourceName: "Crown"))
return result
}()
private lazy var displayNameLabel: UILabel = {
let result = UILabel()
result.textColor = Theme.primaryColor
result.font = UIFont.ows_dynamicTypeSubheadlineClamped
result.lineBreakMode = .byTruncatingTail
return result
}()
// MARK: Initialization
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpViewHierarchy()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
// Make the cell transparent
backgroundColor = .clear
// Set up the profile picture image view
profilePictureImageView.set(.width, to: 36)
profilePictureImageView.set(.height, to: 36)
// Set up the main stack view
let stackView = UIStackView(arrangedSubviews: [ profilePictureImageView, displayNameLabel ])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 16
stackView.set(.height, to: 44)
contentView.addSubview(stackView)
stackView.pin(.leading, to: .leading, of: contentView, withInset: 16)
stackView.pin(.top, to: .top, of: contentView, withInset: 4)
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: 16)
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: 4)
stackView.set(.width, to: UIScreen.main.bounds.width - 2 * 16)
// Set up the moderator icon image view
moderatorIconImageView.set(.width, to: 20)
moderatorIconImageView.set(.height, to: 20)
contentView.addSubview(moderatorIconImageView)
moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureImageView)
moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureImageView, withInset: 3.5)
}
// 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()
profilePictureImageView.image = profilePicture
let isUserModerator = LokiGroupChatAPI.isUserModerator(user, for: LokiGroupChatAPI.publicChatServerID, on: LokiGroupChatAPI.publicChatServer)
moderatorIconImageView.isHidden = !isUserModerator || !hasGroupContext
}
}
}

View File

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

View File

@ -22,16 +22,21 @@ extension UIView {
}
}
func pin(_ constraineeEdge: HorizontalEdge, to constrainerEdge: HorizontalEdge, of view: UIView, withInset inset: CGFloat) {
func pin(_ constraineeEdge: HorizontalEdge, to constrainerEdge: HorizontalEdge, of view: UIView, withInset inset: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
anchor(from: constraineeEdge).constraint(equalTo: view.anchor(from: constrainerEdge), constant: inset).isActive = true
}
func pin(_ constraineeEdge: VerticalEdge, to constrainerEdge: VerticalEdge, of view: UIView, withInset inset: CGFloat) {
func pin(_ constraineeEdge: VerticalEdge, to constrainerEdge: VerticalEdge, of view: UIView, withInset inset: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
anchor(from: constraineeEdge).constraint(equalTo: view.anchor(from: constrainerEdge), constant: inset).isActive = true
}
func pin(to view: UIView) {
[ HorizontalEdge.leading, HorizontalEdge.trailing ].forEach { pin($0, to: $0, of: view) }
[ VerticalEdge.top, VerticalEdge.bottom ].forEach { pin($0, to: $0, of: view) }
}
func center(_ direction: Direction, in view: UIView) {
translatesAutoresizingMaskIntoConstraints = false
switch direction {

View File

@ -676,12 +676,15 @@ NS_ASSUME_NONNULL_BEGIN
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSent;
}
[self.class loadForTextDisplay:self.bodyTextView
displayableText:self.displayableBodyText
searchText:self.delegate.lastSearchedText
textColor:self.bodyTextColor
font:self.textMessageFont
shouldIgnoreEvents:shouldIgnoreEvents];
shouldIgnoreEvents:shouldIgnoreEvents
thread:self.viewItem.interaction.thread
isOutgoing:[self.viewItem.interaction isKindOfClass:TSOutgoingMessage.self]];
}
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
@ -690,11 +693,12 @@ NS_ASSUME_NONNULL_BEGIN
textColor:(UIColor *)textColor
font:(UIFont *)font
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
thread:(TSThread *)thread
isOutgoing:(BOOL)isOutgoing
{
textView.hidden = NO;
textView.textColor = textColor;
// Honor dynamic type in the message bodies.
textView.font = font;
textView.linkTextAttributes = @{
NSForegroundColorAttributeName : textColor,
@ -703,24 +707,58 @@ NS_ASSUME_NONNULL_BEGIN
textView.shouldIgnoreEvents = shouldIgnoreEvents;
NSString *text = displayableText.displayText;
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc]
initWithString:text
attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
NSError *error1;
NSRegularExpression *regex1 = [[NSRegularExpression alloc] initWithPattern:@"@\\w*" 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];
}
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
NSString *searchableText = [FullTextSearchFinder normalizeWithText:searchText];
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];
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];
}
}

View File

@ -5,9 +5,11 @@
NS_ASSUME_NONNULL_BEGIN
@class ConversationStyle;
@class LKUserSelectionView;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class SignalAttachment;
@class TSThread;
@protocol ConversationInputToolbarDelegate <NSObject>
@ -27,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView;
@end
#pragma mark -
@ -80,6 +84,12 @@ NS_ASSUME_NONNULL_BEGIN
- (void)hideInputMethod;
#pragma mark - User Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread;
- (void)hideUserSelectionView;
@end
NS_ASSUME_NONNULL_END

View File

@ -50,7 +50,8 @@ const CGFloat kMaxTextViewHeight = 98;
@interface ConversationInputToolbar () <ConversationTextViewToolbarDelegate,
QuotedReplyPreviewDelegate,
LinkPreviewViewDraftDelegate>
LinkPreviewViewDraftDelegate,
LKUserSelectionViewDelegate>
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
@ -85,6 +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;
@end
@ -219,6 +222,14 @@ const CGFloat kMaxTextViewHeight = 98;
[vStackWrapper setContentHuggingHorizontalLow];
[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;
// H Stack
_hStack = [[UIStackView alloc]
initWithArrangedSubviews:@[ /*self.attachmentButton,*/ vStackWrapper, /*self.voiceMemoButton,*/ self.sendButton ]];
@ -229,7 +240,7 @@ const CGFloat kMaxTextViewHeight = 98;
self.hStack.spacing = 8;
[self addSubview:self.hStack];
[self.hStack autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.hStack autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.userSelectionView];
[self.hStack autoPinEdgeToSuperviewSafeArea:ALEdgeBottom];
[self.hStack setContentHuggingHorizontalLow];
[self.hStack setCompressionResistanceHorizontalLow];
@ -1078,6 +1089,29 @@ const CGFloat kMaxTextViewHeight = 98;
self.borderView.hidden = YES;
}
#pragma mark - User Selection View
- (void)showUserSelectionViewFor:(NSArray<NSString *> *)users in:(TSThread *)thread
{
self.userSelectionView.hasGroupContext = thread.isGroupThread; // Must happen before setting the users
self.userSelectionView.users = users;
self.userSelectionViewSizeConstraint.constant = 10 + MIN(users.count, 4) * 52;
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)hideUserSelectionView
{
self.userSelectionViewSizeConstraint.constant = 0;
[self setNeedsLayout];
[self layoutIfNeeded];
}
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView
{
[self.inputToolbarDelegate handleUserSelected:user from:userSelectionView];
}
@end
NS_ASSUME_NONNULL_END

View File

@ -213,6 +213,8 @@ typedef enum : NSUInteger {
@property (nonatomic) CGFloat extraContentInsetPadding;
@property (nonatomic) CGFloat contentInsetBottom;
@property (nonatomic) NSInteger mentionStartIndex;
@end
#pragma mark -
@ -256,6 +258,8 @@ typedef enum : NSUInteger {
_recordVoiceNoteAudioActivity = [[OWSAudioActivity alloc] initWithAudioDescription:audioActivityDescription behavior:OWSAudioBehavior_PlayAndRecord];
self.scrollContinuity = kScrollContinuityBottom;
_mentionStartIndex = -1;
}
#pragma mark - Dependencies
@ -523,6 +527,8 @@ typedef enum : NSUInteger {
selector:@selector(reloadTimerDidFire)
userInfo:nil
repeats:YES];
[LKAPI populateUserIDCacheIfNeededFor:thread.uniqueId in:nil];
}
- (void)dealloc
@ -596,7 +602,7 @@ typedef enum : NSUInteger {
[super viewDidLoad];
[self createContents];
[self registerCellClasses];
[self createConversationScrollButtons];
@ -668,7 +674,7 @@ typedef enum : NSUInteger {
self.inputToolbar.inputTextViewDelegate = self;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _inputToolbar);
[self updateInputToolbar];
self.loadMoreHeader = [UILabel new];
self.loadMoreHeader.text = NSLocalizedString(@"CONVERSATION_VIEW_LOADING_MORE_MESSAGES",
@"Indicates that the app is loading more messages in this conversation.");
@ -3767,6 +3773,32 @@ typedef enum : NSUInteger {
if (textView.text.length > 0) {
[self.typingIndicators didStartTypingOutgoingInputInThread:self.thread];
}
NSUInteger currentEndIndex = (textView.text.length != 0) ? textView.text.length - 1 : 0;
unichar lastCharacter = [textView.text characterAtIndex:currentEndIndex];
NSMutableCharacterSet *allowedCharacters = NSMutableCharacterSet.lowercaseLetterCharacterSet;
[allowedCharacters formUnionWithCharacterSet:NSCharacterSet.uppercaseLetterCharacterSet];
if (lastCharacter == '@') {
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:@"" in:self.thread.uniqueId];
self.mentionStartIndex = (NSInteger)currentEndIndex + 1;
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
} else if (![allowedCharacters characterIsMember:lastCharacter]) {
self.mentionStartIndex = -1;
[self.inputToolbar hideUserSelectionView];
} else {
if (self.mentionStartIndex != -1) {
NSString *query = [textView.text substringFromIndex:(NSUInteger)self.mentionStartIndex];
NSArray<NSString *> *userIDs = [LKAPI getUserIDsFor:query in:self.thread.uniqueId];
[self.inputToolbar showUserSelectionViewFor:userIDs in:self.thread];
}
}
}
- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView
{
NSString *oldText = self.inputToolbar.messageText;
NSUInteger mentionStartIndex = (NSUInteger)self.mentionStartIndex;
NSString *newText = [oldText stringByReplacingCharactersInRange:NSMakeRange(mentionStartIndex, oldText.length - mentionStartIndex) withString:user];
[self.inputToolbar setMessageText:newText animated:NO];
}
- (void)inputTextViewSendMessagePressed

View File

@ -3,6 +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
// MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared()
@ -14,8 +15,11 @@ public final class LokiAPI : NSObject {
private static let defaultTimeout: TimeInterval = 20
private static let longPollingTimeout: TimeInterval = 40
private static let deviceLinkUpdateInterval: TimeInterval = 8 * 60
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
private static var userIDScanLimit: UInt = 4096
internal static var powDifficulty: UInt = 4
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
// MARK: Types
public typealias RawResponse = Any
@ -260,10 +264,7 @@ public final class LokiAPI : NSObject {
}
}
// MARK: Caching
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
// MARK: Message Hash Caching
private static func getLastMessageHashValue(for target: LokiAPITarget) -> String? {
var result: String? = nil
// Uses a read/write connection because getting the last message hash value also removes expired messages as needed
@ -293,6 +294,63 @@ public final class LokiAPI : NSObject {
transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection)
}
}
// 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
} else {
userIDCache[threadID] = [ userHexEncodedPublicKey ]
}
}
@objc public static func getUserIDs(for query: String, in threadID: String) -> [String] {
// Prepare
guard let cache = userIDCache[threadID] else { return [] }
var candidates: [(id: String, displayName: String)] = []
// 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)
}
}
// Sort alphabetically first
candidates.sort { $0.displayName < $1.displayName }
if query.count >= 2 {
// Filter out any non-matching candidates
candidates = candidates.filter { $0.displayName.contains(query) }
// Sort based on where in the candidate the query occurs
candidates.sort { $0.displayName.range(of: query)!.lowerBound < $1.displayName.range(of: query)!.lowerBound }
}
// 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
}
@objc public static func populateUserIDCacheIfNeeded(for threadID: String, in transaction: YapDatabaseReadWriteTransaction? = nil) {
guard userIDCache[threadID] == nil else { return }
var result: Set<String> = []
func populate(in transaction: YapDatabaseReadWriteTransaction) {
guard let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
let interactions = transaction.ext(TSMessageDatabaseViewExtensionName) as! YapDatabaseViewTransaction
interactions.enumerateKeysAndObjects(inGroup: threadID) { _, _, object, index, _ in
guard let message = object as? TSIncomingMessage, index < userIDScanLimit else { return }
result.insert(message.authorId)
}
}
if let transaction = transaction {
populate(in: transaction)
} else {
storage.dbReadWriteConnection.readWrite { transaction in
populate(in: transaction)
}
}
result.insert(userHexEncodedPublicKey)
userIDCache[threadID] = result
}
}
// MARK: Error Handling

View File

@ -1412,16 +1412,22 @@ NS_ASSUME_NONNULL_BEGIN
(unsigned long)timestamp);
return nil;
}
// Loki: Cache the user hex encoded public key (for mentions)
[LKAPI populateUserIDCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
[LKAPI cache:incomingMessage.authorId for:oldGroupThread.uniqueId];
[self finalizeIncomingMessage:incomingMessage
thread:oldGroupThread
envelope:envelope
transaction:transaction];
// Loki: Map the message ID to the message server ID if needed
if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) {
[self.primaryStorage setIDForMessageWithServerID:dataMessage.publicChatInfo.serverID to:incomingMessage.uniqueId in:transaction];
}
// Loki: Generate a link preview if needed
dispatch_async(dispatch_get_main_queue(), ^{
NSString *linkPreviewURL = [OWSLinkPreview previewURLForRawBodyText:incomingMessage.body];
if (linkPreviewURL != nil) {