From cf07fc1b1a509ec195cf23bb3f8728b4825b2606 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Mon, 1 Mar 2021 13:15:54 +1100 Subject: [PATCH] Implement nicknames --- Session.xcodeproj/project.pbxproj | 4 + .../OWSConversationSettingsViewController.m | 107 ++++++++++++++++-- .../ConversationTitleView.swift | 6 +- .../Database/Notification+Contacts.swift | 10 ++ .../Database/Storage+Contacts.swift | 3 + SessionSnodeKit/OnionRequestAPI.swift | 2 +- SessionUIKit/Components/TextField.swift | 6 + 7 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 SessionMessagingKit/Database/Notification+Contacts.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d5c795d7d..8658c1d72 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -285,6 +285,7 @@ B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */; }; B8F5F52925EC4F8A003BF8D4 /* BlockListUIUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F52825EC4F8A003BF8D4 /* BlockListUIUtils.m */; }; B8F5F54E25EC50A5003BF8D4 /* BlockListUIUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = B8F5F52725EC4F6A003BF8D4 /* BlockListUIUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */; }; B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; }; @@ -1279,6 +1280,7 @@ B8D8F1EF256621180092EF10 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "MessageSender+Convenience.swift"; path = "../../SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift"; sourceTree = ""; }; B8F5F52725EC4F6A003BF8D4 /* BlockListUIUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlockListUIUtils.h; sourceTree = ""; }; B8F5F52825EC4F8A003BF8D4 /* BlockListUIUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BlockListUIUtils.m; sourceTree = ""; }; + B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Contacts.swift"; sourceTree = ""; }; B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = ""; }; B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = ""; }; B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = ""; }; @@ -2627,6 +2629,7 @@ C32C5BCB256DC818003C73A2 /* Database */ = { isa = PBXGroup; children = ( + B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */, C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */, C33FDB07255A580700E217F9 /* OWSBackupFragment.m */, C33FDA67255A57F900E217F9 /* OWSPrimaryStorage.h */, @@ -4797,6 +4800,7 @@ B8856E94256F1C37001CE70E /* OWSSounds.m in Sources */, C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */, C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */, + B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */, C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */, C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index 36a6e09d6..53be62021 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -13,7 +13,6 @@ #import #import #import -#import #import #import #import @@ -41,6 +40,10 @@ CGFloat kIconViewLength = 24; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @property (nonatomic, readonly) UIImageView *avatarView; @property (nonatomic, readonly) UILabel *disappearingMessagesDurationLabel; +@property (nonatomic) UILabel *displayNameLabel; +@property (nonatomic) SNTextField *displayNameTextField; +@property (nonatomic) UIView *displayNameContainer; +@property (nonatomic) BOOL isEditingDisplayName; @end @@ -209,6 +212,32 @@ CGFloat kIconViewLength = 24; { [super viewDidLoad]; + self.displayNameLabel = [UILabel new]; + self.displayNameLabel.textColor = LKColors.text; + self.displayNameLabel.font = [UIFont boldSystemFontOfSize:LKValues.largeFontSize]; + self.displayNameLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.displayNameLabel.textAlignment = NSTextAlignmentCenter; + + self.displayNameTextField = [[SNTextField alloc] initWithPlaceholder:@"Enter a name" usesDefaultHeight:NO]; + self.displayNameTextField.textAlignment = NSTextAlignmentCenter; + self.displayNameTextField.accessibilityLabel = @"Edit name text field"; + self.displayNameTextField.alpha = 0; + + self.displayNameContainer = [UIView new]; + self.displayNameContainer.accessibilityLabel = @"Edit name text field"; + self.displayNameContainer.isAccessibilityElement = YES; + + [self.displayNameContainer autoSetDimension:ALDimensionHeight toSize:40]; + [self.displayNameContainer addSubview:self.displayNameLabel]; + [self.displayNameLabel autoPinToEdgesOfView:self.displayNameContainer]; + [self.displayNameContainer addSubview:self.displayNameTextField]; + [self.displayNameTextField autoPinToEdgesOfView:self.displayNameContainer]; + + if ([self.thread isKindOfClass:TSContactThread.class]) { + UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showEditNameUI)]; + [self.displayNameContainer addGestureRecognizer:tapGestureRecognizer]; + } + self.tableView.estimatedRowHeight = 45; self.tableView.rowHeight = UITableViewAutomaticDimension; @@ -683,13 +712,13 @@ CGFloat kIconViewLength = 24; [profilePictureView autoSetDimension:ALDimensionHeight toSize:size]; [profilePictureView addGestureRecognizer:profilePictureTapGestureRecognizer]; - UILabel *titleView = [UILabel new]; - titleView.textColor = LKColors.text; - titleView.font = [UIFont boldSystemFontOfSize:LKValues.largeFontSize]; - titleView.lineBreakMode = NSLineBreakByTruncatingTail; - titleView.text = (self.threadName != nil && self.threadName.length > 0) ? self.threadName : @"Anonymous"; + self.displayNameLabel.text = (self.threadName != nil && self.threadName.length > 0) ? self.threadName : @"Anonymous"; + if ([self.thread isKindOfClass:TSContactThread.class]) { + UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showEditNameUI)]; + [self.displayNameContainer addGestureRecognizer:tapGestureRecognizer]; + } - UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ profilePictureView, titleView ]]; + UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[ profilePictureView, self.displayNameContainer ]]; stackView.axis = UILayoutConstraintAxisVertical; stackView.spacing = LKValues.mediumSpacing; stackView.distribution = UIStackViewDistributionEqualCentering; @@ -1072,6 +1101,70 @@ CGFloat kIconViewLength = 24; [self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self]; } +- (void)hideEditNameUI +{ + self.isEditingDisplayName = NO; +} + +- (void)showEditNameUI +{ + self.isEditingDisplayName = YES; +} + +- (void)setIsEditingDisplayName:(BOOL)isEditingDisplayName +{ + _isEditingDisplayName = isEditingDisplayName; + + [self updateNavBarButtons]; + + [UIView animateWithDuration:0.25 animations:^{ + self.displayNameLabel.alpha = self.isEditingDisplayName ? 0 : 1; + self.displayNameTextField.alpha = self.isEditingDisplayName ? 1 : 0; + }]; + if (self.isEditingDisplayName) { + [self.displayNameTextField becomeFirstResponder]; + } else { + [self.displayNameTextField resignFirstResponder]; + } +} + +- (void)saveName +{ + if (![self.thread isKindOfClass:TSContactThread.class]) { return; } + SNContact *contact = [LKStorage.shared getContactWithSessionID:self.thread.contactIdentifier]; + if (contact == nil) { return; } + NSString *text = [self.displayNameTextField.text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + contact.nickname = text.length > 0 ? text : nil; + [LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [LKStorage.shared setContact:contact usingTransaction:transaction]; + }]; + self.displayNameLabel.text = text.length > 0 ? text : contact.name; + [self hideEditNameUI]; +} + +- (void)updateNavBarButtons +{ + if (self.isEditingDisplayName) { + UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(hideEditNameUI)]; + cancelButton.tintColor = LKColors.text; + cancelButton.accessibilityLabel = @"Cancel button"; + cancelButton.isAccessibilityElement = YES; + self.navigationItem.leftBarButtonItem = cancelButton; + UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(saveName)]; + doneButton.tintColor = LKColors.text; + doneButton.accessibilityLabel = @"Done button"; + doneButton.isAccessibilityElement = YES; + self.navigationItem.rightBarButtonItem = doneButton; + } else { + self.navigationItem.leftBarButtonItem = nil; + UIBarButtonItem *editButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(showEditNameUI)]; + editButton.tintColor = LKColors.text; + editButton.accessibilityLabel = @"Done button"; + editButton.isAccessibilityElement = YES; + self.navigationItem.rightBarButtonItem = editButton; + } +} + #pragma mark - Notifications - (void)identityStateDidChange:(NSNotification *)notification diff --git a/Session/Conversations/Views & Modals/ConversationTitleView.swift b/Session/Conversations/Views & Modals/ConversationTitleView.swift index 1bb355317..20515b8a5 100644 --- a/Session/Conversations/Views & Modals/ConversationTitleView.swift +++ b/Session/Conversations/Views & Modals/ConversationTitleView.swift @@ -46,8 +46,10 @@ final class ConversationTitleView : UIView { stackView.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0) addSubview(stackView) stackView.pin(to: self) - NotificationCenter.default.addObserver(self, selector: #selector(update), name: Notification.Name.groupThreadUpdated, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(update), name: Notification.Name.muteSettingUpdated, object: nil) + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.groupThreadUpdated, object: nil) + notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.muteSettingUpdated, object: nil) + notificationCenter.addObserver(self, selector: #selector(update), name: Notification.Name.contactUpdated, object: nil) update() } diff --git a/SessionMessagingKit/Database/Notification+Contacts.swift b/SessionMessagingKit/Database/Notification+Contacts.swift new file mode 100644 index 000000000..05f3cfca1 --- /dev/null +++ b/SessionMessagingKit/Database/Notification+Contacts.swift @@ -0,0 +1,10 @@ + +public extension Notification.Name { + + static let contactUpdated = Notification.Name("contactUpdated") +} + +@objc public extension NSNotification { + + @objc static let contactUpdated = Notification.Name.contactUpdated.rawValue as NSString +} diff --git a/SessionMessagingKit/Database/Storage+Contacts.swift b/SessionMessagingKit/Database/Storage+Contacts.swift index df8744fa4..448afc6f0 100644 --- a/SessionMessagingKit/Database/Storage+Contacts.swift +++ b/SessionMessagingKit/Database/Storage+Contacts.swift @@ -15,6 +15,9 @@ extension Storage { @objc(setContact:usingTransaction:) public func setContact(_ contact: Contact, using transaction: Any) { (transaction as! YapDatabaseReadWriteTransaction).setObject(contact, forKey: contact.sessionID, inCollection: Storage.contactCollection) + DispatchQueue.main.async { + NotificationCenter.default.post(name: .contactUpdated, object: contact.sessionID) + } } public func getAllContacts() -> Set { diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 47705ed84..0f96d8926 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -285,7 +285,7 @@ public enum OnionRequestAPI { }.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) } } - // MARK: Internal API + // MARK: Public API /// Sends an onion request to `snode`. Builds new paths as needed. public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] diff --git a/SessionUIKit/Components/TextField.swift b/SessionUIKit/Components/TextField.swift index 42dfb9b39..f8eb37270 100644 --- a/SessionUIKit/Components/TextField.swift +++ b/SessionUIKit/Components/TextField.swift @@ -1,5 +1,6 @@ import UIKit +@objc(SNTextField) public final class TextField : UITextField { private let usesDefaultHeight: Bool private let height: CGFloat @@ -9,6 +10,11 @@ public final class TextField : UITextField { static let height: CGFloat = isIPhone5OrSmaller ? CGFloat(48) : CGFloat(80) public static let cornerRadius: CGFloat = 8 + @objc(initWithPlaceholder:usesDefaultHeight:) + public convenience init(placeholder: String, usesDefaultHeight: Bool) { + self.init(placeholder: placeholder, usesDefaultHeight: usesDefaultHeight, customHeight: nil, customHorizontalInset: nil, customVerticalInset: nil) + } + public init(placeholder: String, usesDefaultHeight: Bool = true, customHeight: CGFloat? = nil, customHorizontalInset: CGFloat? = nil, customVerticalInset: CGFloat? = nil) { self.usesDefaultHeight = usesDefaultHeight self.height = customHeight ?? TextField.height