diff --git a/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift b/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift index 5bbfdf560..c94358c65 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift +++ b/Signal/src/ViewControllers/ConversationView/ConversationHeaderView.swift @@ -38,72 +38,79 @@ public class ConversationHeaderView: UIStackView { } } - public let titlePrimaryFont: UIFont = UIFont.ows_boldFont(withSize: 20) - public let titleSecondaryFont: UIFont = UIFont.ows_regularFont(withSize: 11) + public var avatarImage: UIImage? { + get { + return self.avatarView.image + } + set { + self.avatarView.image = newValue + } + } + + public let titlePrimaryFont: UIFont = UIFont.ows_boldFont(withSize: 17) + public let titleSecondaryFont: UIFont = UIFont.ows_regularFont(withSize: 9) public let subtitleFont: UIFont = UIFont.ows_regularFont(withSize: 12) // public let columns: UIStackView // public let textRows: UIStackView private let titleLabel: UILabel private let subtitleLabel: UILabel + private let avatarView: AvatarImageView - override init(frame: CGRect) { + public required init(thread: TSThread, contactsManager: OWSContactsManager) { - // TODO -// let avatarView: UIImageView = UIImageView() + let avatarView = ConversationAvatarImageView(thread: thread, diameter: 36, contactsManager: contactsManager) + self.avatarView = avatarView + // remove default border on avatarView + avatarView.layer.borderWidth = 0 titleLabel = UILabel() titleLabel.textColor = .white titleLabel.lineBreakMode = .byTruncatingTail titleLabel.font = titlePrimaryFont - titleLabel.setContentHuggingHigh() subtitleLabel = UILabel() subtitleLabel.textColor = .white subtitleLabel.lineBreakMode = .byTruncatingTail subtitleLabel.font = subtitleFont - subtitleLabel.setContentHuggingHigh() -// textRows = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) -// textRows.axis = .vertical -// textRows.alignment = .leading + let textRows = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel]) + textRows.axis = .vertical + textRows.alignment = .leading + textRows.distribution = .fillProportionally + + textRows.layoutMargins = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4) + textRows.isLayoutMarginsRelativeArrangement = true -// columns = UIStackView(arrangedSubviews: [avatarView, textRows]) + // low content hugging so that the text rows push container to the right bar button item(s) + textRows.setContentHuggingLow() - super.init(frame: frame) + super.init(frame: .zero) + + self.layoutMargins = UIEdgeInsets(top: 4, left: 2, bottom: 4, right: 2) + self.isLayoutMarginsRelativeArrangement = true // needed for proper layout on iOS10 self.translatesAutoresizingMaskIntoConstraints = false - self.axis = .vertical - self.distribution = .fillProportionally - self.alignment = .leading + self.axis = .horizontal + self.alignment = .center self.spacing = 0 - self.addArrangedSubview(titleLabel) - self.addArrangedSubview(subtitleLabel) + self.addArrangedSubview(avatarView) + self.addArrangedSubview(textRows) let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView)) self.addGestureRecognizer(tapGesture) - -// titleLabel.setCompressionResistanceHigh() -// subtitleLabel.setCompressionResistanceHigh() -// self.setCompressionResistanceHigh() -// self.setContentHuggingLow() - -// self.layoutIfNeeded() -// sizeToFit() -// -// self.translatesAutoresizingMaskIntoConstraints = true - -// self.addSubview(columns) -// columns.autoPinEdgesToSuperviewEdges() -// self.addRedBorderRecursively() } required public init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + required public override init(frame: CGRect) { + fatalError("init(frame:) has not been implemented") + } + public override var intrinsicContentSize: CGSize { // Grow to fill as much of the navbar as possible. if #available(iOS 11, *) { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index cf2ef3290..e9c0a5cb7 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -489,6 +489,7 @@ typedef enum : NSUInteger { [self createConversationScrollButtons]; [self createHeaderViews]; + // [self createBackButton]; [self addNotificationListeners]; [self loadDraftInCompose]; @@ -1114,22 +1115,26 @@ typedef enum : NSUInteger { - (void)createHeaderViews { - _backButtonUnreadCountView = [UIView new]; - _backButtonUnreadCountView.layer.cornerRadius = self.unreadCountViewDiameter / 2; - _backButtonUnreadCountView.backgroundColor = [UIColor redColor]; - _backButtonUnreadCountView.hidden = YES; - _backButtonUnreadCountView.userInteractionEnabled = NO; - - _backButtonUnreadCountLabel = [UILabel new]; - _backButtonUnreadCountLabel.backgroundColor = [UIColor clearColor]; - _backButtonUnreadCountLabel.textColor = [UIColor whiteColor]; - _backButtonUnreadCountLabel.font = [UIFont systemFontOfSize:11]; - _backButtonUnreadCountLabel.textAlignment = NSTextAlignmentCenter; - + // _backButtonUnreadCountView = [UIView new]; + // _backButtonUnreadCountView.layer.cornerRadius = self.unreadCountViewDiameter / 2; + // _backButtonUnreadCountView.backgroundColor = [UIColor redColor]; + // _backButtonUnreadCountView.hidden = YES; + // _backButtonUnreadCountView.userInteractionEnabled = NO; + // + // _backButtonUnreadCountLabel = [UILabel new]; + // _backButtonUnreadCountLabel.backgroundColor = [UIColor clearColor]; + // _backButtonUnreadCountLabel.textColor = [UIColor whiteColor]; + // _backButtonUnreadCountLabel.font = [UIFont systemFontOfSize:11]; + // _backButtonUnreadCountLabel.textAlignment = NSTextAlignmentCenter; + // - ConversationHeaderView *headerView = [ConversationHeaderView new]; + ConversationHeaderView *headerView = + [[ConversationHeaderView alloc] initWithThread:self.thread contactsManager:self.contactsManager]; headerView.delegate = self; + // UIImage *avatarImage = [OWSAvatarBuilder buildImageForThread:self.thread diameter:36 + // contactsManager:self.contactsManager]; headerView.avatarImage = avatarImage; + self.headerView = headerView; #ifdef USE_DEBUG_UI @@ -3814,21 +3819,21 @@ typedef enum : NSUInteger { - (void)setBackButtonUnreadCount:(NSUInteger)unreadCount { - OWSAssertIsOnMainThread(); - if (_backButtonUnreadCount == unreadCount) { - // No need to re-render same count. - return; - } - _backButtonUnreadCount = unreadCount; - - OWSAssert(_backButtonUnreadCountView != nil); - _backButtonUnreadCountView.hidden = unreadCount <= 0; - - OWSAssert(_backButtonUnreadCountLabel != nil); - - // Max out the unread count at 99+. - const NSUInteger kMaxUnreadCount = 99; - _backButtonUnreadCountLabel.text = [OWSFormat formatInt:(int)MIN(kMaxUnreadCount, unreadCount)]; + // OWSAssertIsOnMainThread(); + // if (_backButtonUnreadCount == unreadCount) { + // // No need to re-render same count. + // return; + // } + // _backButtonUnreadCount = unreadCount; + // + // OWSAssert(_backButtonUnreadCountView != nil); + // _backButtonUnreadCountView.hidden = unreadCount <= 0; + // + // OWSAssert(_backButtonUnreadCountLabel != nil); + // + // // Max out the unread count at 99+. + // const NSUInteger kMaxUnreadCount = 99; + // _backButtonUnreadCountLabel.text = [OWSFormat formatInt:(int)MIN(kMaxUnreadCount, unreadCount)]; } #pragma mark 3D Touch Preview Actions diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index ba39e7cca..d5dfe5ac5 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -275,12 +275,12 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations } // We don't show any text for the back button, so there's no need to localize it. But because we left align the - // conversation title view, we having a title which is just spaces, we can add tappable padding to the back button. + // conversation title view, we add a little tappable padding after the back button, by having a title of spaces. // Admittedly this is kind of a hack and not super fine grained, but it's simple and results in the interactive pop - // gesture animating our title view properly vs. creating our own back button bar item and adjusting padding that - // way. + // gesture animating our title view nicely vs. creating our own back button bar item with custom padding, which does + // not properly animate with the "swipe to go back" or "swipe left for info" gestures. self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:@" " style:UIBarButtonItemStylePlain target:nil action:nil]; + [[UIBarButtonItem alloc] initWithTitle:@" " style:UIBarButtonItemStylePlain target:nil action:nil]; if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)] && (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable)) { diff --git a/SignalMessaging/Views/AvatarImageView.swift b/SignalMessaging/Views/AvatarImageView.swift index b463d04a5..a3c3c337e 100644 --- a/SignalMessaging/Views/AvatarImageView.swift +++ b/SignalMessaging/Views/AvatarImageView.swift @@ -1,14 +1,127 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import UIKit +@objc +public class ConversationAvatarImageView: AvatarImageView { + + let thread: TSThread + let diameter: UInt + let contactsManager: OWSContactsManager + + // nil if group avatar + let recipientId: String? + + // nil if contact avatar + let groupThreadId: String? + + required public init(thread: TSThread, diameter: UInt, contactsManager: OWSContactsManager) { + self.thread = thread + self.diameter = diameter + self.contactsManager = contactsManager + + switch thread { + case let contactThread as TSContactThread: + self.recipientId = contactThread.contactIdentifier() + self.groupThreadId = nil + case let groupThread as TSGroupThread: + self.recipientId = nil + self.groupThreadId = groupThread.uniqueId + default: + owsFail("in \(#function) unexpected thread type: \(thread)") + self.recipientId = nil + self.groupThreadId = nil + } + + super.init(frame: .zero) + + if recipientId != nil { + NotificationCenter.default.addObserver(self, selector: #selector(handleOtherUsersProfileChanged(notification:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(handleSignalAccountsChanged(notification:)), name: NSNotification.Name.OWSContactsManagerSignalAccountsDidChange, object: nil) + } + + if groupThreadId != nil { + NotificationCenter.default.addObserver(self, selector: #selector(handleGroupAvatarChanged(notification:)), name: .TSGroupThreadAvatarChanged, object: nil) + } + + // TODO group avatar changed + self.updateImage() + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func handleSignalAccountsChanged(notification: Notification) { + Logger.debug("\(self.logTag) in \(#function)") + + // It would be nice if we could do this only if *this* user changed, + // but currently this is a course grained notification. + + self.updateImage() + } + + func handleOtherUsersProfileChanged(notification: Notification) { + Logger.debug("\(self.logTag) in \(#function)") + + guard let changedRecipientId = notification.userInfo?[kNSNotificationKey_ProfileRecipientId] as? String else { + owsFail("\(logTag) in \(#function) recipientId was unexpectedly nil") + return + } + + guard let recipientId = self.recipientId else { + // shouldn't call this for group threads + owsFail("\(logTag) in \(#function) contactId was unexpectedly nil") + return + } + + guard recipientId == recipientId else { + // not this avatar + return + } + + self.updateImage() + } + + func handleGroupAvatarChanged(notification: Notification) { + Logger.debug("\(self.logTag) in \(#function)") + + guard let changedGroupThreadId = notification.userInfo?[TSGroupThread_NotificaitonKey_UniqueId] as? String else { + owsFail("\(logTag) in \(#function) groupThreadId was unexpectedly nil") + return + } + + guard let groupThreadId = self.groupThreadId else { + // shouldn't call this for contact threads + owsFail("\(logTag) in \(#function) groupThreadId was unexpectedly nil") + return + } + + guard groupThreadId == changedGroupThreadId else { + // not this avatar + return + } + + thread.reload() + + self.updateImage() + } + + func updateImage() { + Logger.debug("\(self.logTag) in \(#function) updateImage") + + self.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: diameter, contactsManager: contactsManager) + } +} + @objc public class AvatarImageView: UIImageView { public init() { - super.init(frame: CGRect.zero) + super.init(frame: .zero) self.configureView() } @@ -28,6 +141,8 @@ public class AvatarImageView: UIImageView { } func configureView() { + self.autoPinToSquareAspectRatio() + self.layer.minificationFilter = kCAFilterTrilinear self.layer.magnificationFilter = kCAFilterTrilinear self.layer.borderWidth = 0.5 diff --git a/SignalMessaging/utils/OWSAvatarBuilder.m b/SignalMessaging/utils/OWSAvatarBuilder.m index 3fb63777b..55987acd7 100644 --- a/SignalMessaging/utils/OWSAvatarBuilder.m +++ b/SignalMessaging/utils/OWSAvatarBuilder.m @@ -21,6 +21,9 @@ NS_ASSUME_NONNULL_BEGIN diameter:(NSUInteger)diameter contactsManager:(OWSContactsManager *)contactsManager { + OWSAssert(thread); + OWSAssert(contactsManager); + OWSAvatarBuilder *avatarBuilder; if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; diff --git a/SignalServiceKit/src/Contacts/Threads/TSGroupThread.h b/SignalServiceKit/src/Contacts/Threads/TSGroupThread.h index 8c8890bee..981b7d262 100644 --- a/SignalServiceKit/src/Contacts/Threads/TSGroupThread.h +++ b/SignalServiceKit/src/Contacts/Threads/TSGroupThread.h @@ -10,6 +10,9 @@ NS_ASSUME_NONNULL_BEGIN @class TSAttachmentStream; @class YapDatabaseReadWriteTransaction; +extern NSString *const TSGroupThreadAvatarChangedNotification; +extern NSString *const TSGroupThread_NotificaitonKey_UniqueId; + @interface TSGroupThread : TSThread @property (nonatomic, strong) TSGroupModel *groupModel; diff --git a/SignalServiceKit/src/Contacts/Threads/TSGroupThread.m b/SignalServiceKit/src/Contacts/Threads/TSGroupThread.m index 284de2482..51a33c45b 100644 --- a/SignalServiceKit/src/Contacts/Threads/TSGroupThread.m +++ b/SignalServiceKit/src/Contacts/Threads/TSGroupThread.m @@ -12,6 +12,9 @@ NS_ASSUME_NONNULL_BEGIN +NSString *const TSGroupThreadAvatarChangedNotification = @"TSGroupThreadAvatarChangedNotification"; +NSString *const TSGroupThread_NotificaitonKey_UniqueId = @"TSGroupThread_NotificaitonKey_UniqueId"; + @implementation TSGroupThread #define TSGroupThreadPrefix @"g" @@ -192,6 +195,16 @@ NS_ASSUME_NONNULL_BEGIN self.groupModel.groupImage = [attachmentStream image]; [self saveWithTransaction:transaction]; + [transaction addCompletionQueue:nil + completionBlock:^{ + NSDictionary *userInfo = @{ TSGroupThread_NotificaitonKey_UniqueId : self.uniqueId }; + + [[NSNotificationCenter defaultCenter] + postNotificationName:TSGroupThreadAvatarChangedNotification + object:self.uniqueId + userInfo:userInfo]; + }]; + // Avatars are stored directly in the database, so there's no need // to keep the attachment around after assigning the image. [attachmentStream removeWithTransaction:transaction]; diff --git a/SignalServiceKit/src/Storage/TSYapDatabaseObject.h b/SignalServiceKit/src/Storage/TSYapDatabaseObject.h index 1effcaf5f..0958def56 100644 --- a/SignalServiceKit/src/Storage/TSYapDatabaseObject.h +++ b/SignalServiceKit/src/Storage/TSYapDatabaseObject.h @@ -91,6 +91,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)save; +/** + * Assign the latest persisted values from the database. + */ +- (void)reload; + /** * Saves the object with the shared readWrite connection - does not block. * diff --git a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m index 4317e8b7a..fbaf4cf21 100644 --- a/SignalServiceKit/src/Storage/TSYapDatabaseObject.m +++ b/SignalServiceKit/src/Storage/TSYapDatabaseObject.m @@ -223,6 +223,17 @@ NS_ASSUME_NONNULL_BEGIN } } +- (void)reload +{ + TSYapDatabaseObject *latest = [[self class] fetchObjectWithUniqueID:self.uniqueId]; + if (!latest) { + OWSFail(@"%@ in %s `latest` was unexpectedly nil", self.logTag, __PRETTY_FUNCTION__); + return; + } + + [self setValuesForKeysWithDictionary:latest.dictionaryValue]; +} + @end NS_ASSUME_NONNULL_END