Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mentions V1 #56

Merged
merged 5 commits into from
Oct 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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