mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Implement user selection UI
This commit is contained in:
parent
ea3da42faf
commit
10eead529f
9 changed files with 268 additions and 23 deletions
|
@ -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>"; };
|
||||
|
@ -2651,6 +2655,8 @@
|
|||
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
|
||||
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
|
||||
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
|
||||
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */,
|
||||
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */,
|
||||
);
|
||||
path = Loki;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3840,6 +3846,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 */,
|
||||
|
|
133
Signal/src/Loki/UserSelectionView.swift
Normal file
133
Signal/src/Loki/UserSelectionView.swift
Normal 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: 4, leading: 0, bottom: 4, 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
|
||||
}
|
||||
}
|
||||
}
|
6
Signal/src/Loki/UserSelectionViewDelegate.swift
Normal file
6
Signal/src/Loki/UserSelectionViewDelegate.swift
Normal file
|
@ -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,14 +676,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction;
|
||||
shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSent;
|
||||
}
|
||||
NSString *threadID = self.viewItem.interaction.uniqueThreadId;
|
||||
|
||||
[self.class loadForTextDisplay:self.bodyTextView
|
||||
displayableText:self.displayableBodyText
|
||||
searchText:self.delegate.lastSearchedText
|
||||
textColor:self.bodyTextColor
|
||||
font:self.textMessageFont
|
||||
shouldIgnoreEvents:shouldIgnoreEvents
|
||||
threadID:threadID];
|
||||
thread:self.viewItem.interaction.thread];
|
||||
}
|
||||
|
||||
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
|
||||
|
@ -692,7 +692,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
textColor:(UIColor *)textColor
|
||||
font:(UIFont *)font
|
||||
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
|
||||
threadID:(NSString *)threadID
|
||||
thread:(TSThread *)thread
|
||||
{
|
||||
textView.hidden = NO;
|
||||
textView.textColor = textColor;
|
||||
|
@ -709,30 +709,29 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSError *error1;
|
||||
NSRegularExpression *regex1 = [[NSRegularExpression alloc] initWithPattern:@"@\\w*" options:0 error:&error1];
|
||||
OWSAssertDebug(error1 == nil);
|
||||
NSSet<NSString *> *knownUserIDs = LKAPI.userIDCache[threadID];
|
||||
NSSet<NSString *> *knownUserIDs = LKAPI.userIDCache[thread.uniqueId];
|
||||
NSMutableSet<NSValue *> *mentions = [NSMutableSet new];
|
||||
NSTextCheckingResult *match1 = [regex1 firstMatchInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)];
|
||||
if (match1 != nil) {
|
||||
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 = [Environment.shared.contactsManager attributedContactOrProfileNameForPhoneIdentifier:userID primaryFont:font secondaryFont:font].string;
|
||||
if ([userDisplayName isEqual:userID]) {
|
||||
__block NSString *userDisplayName;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
NSString *collection = [NSString stringWithFormat:@"%@.%llu", LKGroupChatAPI.publicChatServer, LKGroupChatAPI.publicChatServerID];
|
||||
NSString *userDisplayNameCandidate = [transaction objectForKey:userID inCollection:collection];
|
||||
if (userDisplayNameCandidate != nil) {
|
||||
userDisplayName = userDisplayNameCandidate;
|
||||
}
|
||||
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; }
|
||||
}
|
||||
|
|
|
@ -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,25 @@ 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 = 4 + MIN(users.count, 4) * 52 + 4;
|
||||
}
|
||||
|
||||
- (void)hideUserSelectionView
|
||||
{
|
||||
self.userSelectionViewSizeConstraint.constant = 0;
|
||||
}
|
||||
|
||||
- (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
|
||||
|
@ -3768,6 +3772,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
|
||||
|
|
|
@ -306,6 +306,30 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
@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> = []
|
||||
|
|
Loading…
Reference in a new issue