Skip to content

Commit

Permalink
Merge pull request #56 from loki-project/mentions
Browse files Browse the repository at this point in the history
Mentions V1
  • Loading branch information
nielsandriesse committed Oct 9, 2019
2 parents cbc1297 + b0e0efc commit 21aa2f6
Show file tree
Hide file tree
Showing 11 changed files with 361 additions and 31 deletions.
8 changes: 8 additions & 0 deletions Signal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -2651,6 +2655,8 @@
B89841E222B7579F00B1BDC6 /* NewConversationVC.swift */,
B8258491230FA5DA001B41CB /* ScanQRCodeVC.h */,
B8258492230FA5E9001B41CB /* ScanQRCodeVC.m */,
B8B26C8E234D629C004ED98C /* UserSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* UserSelectionViewDelegate.swift */,
);
path = Loki;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down
2 changes: 1 addition & 1 deletion Signal/Signal-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
133 changes: 133 additions & 0 deletions Signal/src/Loki/UserSelectionView.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
6 changes: 6 additions & 0 deletions Signal/src/Loki/UserSelectionViewDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

@objc(LKUserSelectionViewDelegate)
protocol UserSelectionViewDelegate {

func handleUserSelected(_ user: String, from userSelectionView: UserSelectionView)
}
9 changes: 7 additions & 2 deletions Signal/src/Loki/Utilities/UIView+Constraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,12 +676,15 @@ - (void)configureBodyTextView
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
Expand All @@ -690,11 +693,12 @@ + (void)loadForTextDisplay:(OWSMessageTextView *)textView
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,
Expand All @@ -703,24 +707,58 @@ + (void)loadForTextDisplay:(OWSMessageTextView *)textView
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];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
NS_ASSUME_NONNULL_BEGIN

@class ConversationStyle;
@class LKUserSelectionView;
@class OWSLinkPreviewDraft;
@class OWSQuotedReplyModel;
@class SignalAttachment;
@class TSThread;

@protocol ConversationInputToolbarDelegate <NSObject>

Expand All @@ -27,6 +29,8 @@ NS_ASSUME_NONNULL_BEGIN

- (void)voiceMemoGestureDidUpdateCancelWithRatioComplete:(CGFloat)cancelAlpha;

- (void)handleUserSelected:(NSString *)user from:(LKUserSelectionView *)userSelectionView;

@end

#pragma mark -
Expand Down Expand Up @@ -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
Loading

0 comments on commit 21aa2f6

Please sign in to comment.