Merge commit '21aa2f646465094c82a1d0062aa74f4a482ca31b' into custom-server
# Conflicts: # Signal.xcodeproj/project.pbxproj
This commit is contained in:
commit
315ba3ecf9
|
@ -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 */,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
@objc(LKUserSelectionViewDelegate)
|
||||
protocol UserSelectionViewDelegate {
|
||||
|
||||
func handleUserSelected(_ user: String, from userSelectionView: UserSelectionView)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue