diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index df9d0ad23..9dbf6c231 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -7,11 +7,13 @@ // Separate iOS Frameworks from other imports. #import "AppSettingsViewController.h" +#import "ContactTableViewCell.h" #import "ConversationViewItem.h" #import "DateUtil.h" #import "DebugUIPage.h" #import "DebugUITableViewController.h" #import "FingerprintViewController.h" +#import "HomeViewCell.h" #import "HomeViewController.h" #import "MediaDetailViewController.h" #import "NotificationSettingsViewController.h" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m index 1a60642fa..2e3323cec 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactShareView.m @@ -235,7 +235,7 @@ NS_ASSUME_NONNULL_BEGIN [stackView autoPinTrailingToSuperviewMarginWithInset:self.iconHMargin]; [stackView autoVCenterInSuperview]; // Ensure that the cell's contents never overflow the cell bounds. - // We pin pin to the superview _edge_ and not _margin_ for the purposes + // We pin to the superview _edge_ and not _margin_ for the purposes // of overflow, so that changes to the margins do not trip these safe guards. [stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; [stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; diff --git a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift index 24b1a7c4d..f0ba2e854 100644 --- a/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift +++ b/Signal/src/ViewControllers/HomeView/ConversationSearchViewController.swift @@ -29,18 +29,22 @@ class ConversationSearchViewController: UITableViewController { case messages } + var blockedPhoneNumberSet = Set() + // MARK: View Lifecyle override func viewDidLoad() { super.viewDidLoad() + let blockingManager = OWSBlockingManager.shared() + blockedPhoneNumberSet = Set(blockingManager.blockedPhoneNumbers()) + tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 60 tableView.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier) - tableView.register(ConversationSearchResultCell.self, forCellReuseIdentifier: ConversationSearchResultCell.reuseIdentifier) - tableView.register(MessageSearchResultCell.self, forCellReuseIdentifier: MessageSearchResultCell.reuseIdentifier) - tableView.register(ContactSearchResultCell.self, forCellReuseIdentifier: ContactSearchResultCell.reuseIdentifier) + tableView.register(HomeViewCell.self, forCellReuseIdentifier: HomeViewCell.cellReuseIdentifier()) + tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: ContactTableViewCell.reuseIdentifier()) } // MARK: UITableViewDelegate @@ -131,7 +135,7 @@ class ConversationSearchViewController: UITableViewController { cell.configure(searchText: searchText) return cell case .conversations: - guard let cell = tableView.dequeueReusableCell(withIdentifier: ConversationSearchResultCell.reuseIdentifier) as? ConversationSearchResultCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeViewCell.cellReuseIdentifier()) as? HomeViewCell else { owsFail("cell was unexpectedly nil") return UITableViewCell() } @@ -140,10 +144,10 @@ class ConversationSearchViewController: UITableViewController { owsFail("searchResult was unexpectedly nil") return UITableViewCell() } - cell.configure(searchResult: searchResult) + cell.configure(withThread: searchResult.thread, contactsManager: contactsManager, blockedPhoneNumber: self.blockedPhoneNumberSet) return cell case .contacts: - guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactSearchResultCell.reuseIdentifier) as? ContactSearchResultCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier()) as? ContactTableViewCell else { owsFail("cell was unexpectedly nil") return UITableViewCell() } @@ -152,11 +156,10 @@ class ConversationSearchViewController: UITableViewController { owsFail("searchResult was unexpectedly nil") return UITableViewCell() } - - cell.configure(searchResult: searchResult) + cell.configure(with: searchResult.signalAccount, contactsManager: contactsManager) return cell case .messages: - guard let cell = tableView.dequeueReusableCell(withIdentifier: MessageSearchResultCell.reuseIdentifier) as? MessageSearchResultCell else { + guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeViewCell.cellReuseIdentifier()) as? HomeViewCell else { owsFail("cell was unexpectedly nil") return UITableViewCell() } @@ -166,7 +169,32 @@ class ConversationSearchViewController: UITableViewController { return UITableViewCell() } - cell.configure(searchResult: searchResult) + var overrideSnippet = NSAttributedString() + var overrideDate: Date? + if searchResult.messageId != nil { + if let messageDate = searchResult.messageDate { + overrideDate = messageDate + } else { + owsFail("\(ConversationSearchViewController.logTag) message search result is missing message timestamp") + } + + // Note that we only use the snippet for message results, + // not conversation results. HomeViewCell will generate + // a snippet for conversations that reflects the latest + // contents. + if let messageSnippet = searchResult.snippet { + overrideSnippet = NSAttributedString(string: messageSnippet) + } else { + owsFail("\(ConversationSearchViewController.logTag) message search result is missing message snippet") + } + } + + cell.configure(withThread: searchResult.thread, + contactsManager: contactsManager, + blockedPhoneNumber: self.blockedPhoneNumberSet, + overrideSnippet: overrideSnippet, + overrideDate: overrideDate) + return cell } } @@ -227,154 +255,6 @@ class ConversationSearchViewController: UITableViewController { } } -class ConversationSearchResultCell: UITableViewCell { - static let reuseIdentifier = "ConversationSearchResultCell" - - let nameLabel: UILabel - let snippetLabel: UILabel - let avatarView: AvatarImageView - let avatarWidth: UInt = 40 - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - self.nameLabel = UILabel() - self.snippetLabel = UILabel() - self.avatarView = AvatarImageView() - avatarView.autoSetDimensions(to: CGSize(width: CGFloat(avatarWidth), height: CGFloat(avatarWidth))) - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - nameLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight() - snippetLabel.font = UIFont.ows_dynamicTypeFootnote - - let textRows = UIStackView(arrangedSubviews: [nameLabel, snippetLabel]) - textRows.axis = .vertical - - let columns = UIStackView(arrangedSubviews: [avatarView, textRows]) - columns.axis = .horizontal - columns.spacing = 8 - - contentView.addSubview(columns) - columns.autoPinEdgesToSuperviewMargins() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var contactsManager: OWSContactsManager { - return Environment.current().contactsManager - } - - func configure(searchResult: ConversationSearchResult) { - self.avatarView.image = OWSAvatarBuilder.buildImage(thread: searchResult.thread.threadRecord, diameter: avatarWidth, contactsManager: self.contactsManager) - self.nameLabel.text = searchResult.thread.name - self.snippetLabel.text = searchResult.snippet - } -} - -class MessageSearchResultCell: UITableViewCell { - static let reuseIdentifier = "MessageSearchResultCell" - - let nameLabel: UILabel - let snippetLabel: UILabel - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - self.nameLabel = UILabel() - self.snippetLabel = UILabel() - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - nameLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight() - snippetLabel.font = UIFont.ows_dynamicTypeFootnote - - let textRows = UIStackView(arrangedSubviews: [nameLabel, snippetLabel]) - textRows.axis = .vertical - - contentView.addSubview(textRows) - textRows.autoPinEdgesToSuperviewMargins() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure(searchResult: ConversationSearchResult) { - self.nameLabel.text = searchResult.thread.name - - guard let snippet = searchResult.snippet else { - self.snippetLabel.text = nil - return - } - - guard let encodedString = snippet.data(using: .utf8) else { - self.snippetLabel.text = nil - return - } - - // Bold snippet text - do { - - // FIXME - The snippet marks up the matched search text with tags. - // We can parse this into an attributed string, but it also takes on an undesirable font. - // We want to apply our own font without clobbering bold in the process - maybe by enumerating and inspecting the attributes? Or maybe we can pass in a base font? - let attributedSnippet = try NSMutableAttributedString(data: encodedString, - options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html], - documentAttributes: nil) - attributedSnippet.addAttribute(NSAttributedStringKey.font, value: self.snippetLabel.font, range: NSRange(location: 0, length: attributedSnippet.length)) - - self.snippetLabel.attributedText = attributedSnippet - } catch { - owsFail("failed to generate snippet: \(error)") - } - } -} - -class ContactSearchResultCell: UITableViewCell { - static let reuseIdentifier = "ContactSearchResultCell" - - let nameLabel: UILabel - let snippetLabel: UILabel - let avatarView: AvatarImageView - let avatarWidth: UInt = 40 - - override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - self.nameLabel = UILabel() - self.snippetLabel = UILabel() - self.avatarView = AvatarImageView() - avatarView.autoSetDimensions(to: CGSize(width: CGFloat(avatarWidth), height: CGFloat(avatarWidth))) - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - nameLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight() - snippetLabel.font = UIFont.ows_dynamicTypeFootnote - - let textRows = UIStackView(arrangedSubviews: [nameLabel, snippetLabel]) - textRows.axis = .vertical - - let columns = UIStackView(arrangedSubviews: [avatarView, textRows]) - columns.axis = .horizontal - columns.spacing = 8 - - contentView.addSubview(columns) - columns.autoPinEdgesToSuperviewMargins() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - var contactsManager: OWSContactsManager { - return Environment.current().contactsManager - } - - func configure(searchResult: ContactSearchResult) { - let avatarBuilder = OWSContactAvatarBuilder.init(signalId: searchResult.recipientId, diameter: avatarWidth, contactsManager: contactsManager) - self.avatarView.image = avatarBuilder.build() - self.nameLabel.text = self.contactsManager.displayName(forPhoneIdentifier: searchResult.recipientId) - self.snippetLabel.text = searchResult.recipientId - } -} - class EmptySearchResultCell: UITableViewCell { static let reuseIdentifier = "EmptySearchResultCell" diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.h b/Signal/src/ViewControllers/HomeView/HomeViewCell.h index 054b9fcb5..c7f249317 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.h +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.h @@ -10,14 +10,18 @@ NS_ASSUME_NONNULL_BEGIN @interface HomeViewCell : UITableViewCell -+ (CGFloat)rowHeight; - + (NSString *)cellReuseIdentifier; - (void)configureWithThread:(ThreadViewModel *)thread contactsManager:(OWSContactsManager *)contactsManager blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet; +- (void)configureWithThread:(ThreadViewModel *)thread + contactsManager:(OWSContactsManager *)contactsManager + blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet + overrideSnippet:(nullable NSAttributedString *)overrideSnippet + overrideDate:(nullable NSDate *)overrideDate; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/HomeView/HomeViewCell.m b/Signal/src/ViewControllers/HomeView/HomeViewCell.m index 283f35887..bef284b6c 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewCell.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewCell.m @@ -59,6 +59,8 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(!self.avatarView); + const CGFloat kMinVMargin = 5; + [self setTranslatesAutoresizingMaskIntoConstraints:NO]; self.layoutMargins = UIEdgeInsetsMake(0, self.cellHMargin, 0, self.cellHMargin); self.contentView.layoutMargins = UIEdgeInsetsZero; @@ -77,6 +79,13 @@ NS_ASSUME_NONNULL_BEGIN [self.avatarView autoVCenterInSuperview]; [self.avatarView setContentHuggingHigh]; [self.avatarView setCompressionResistanceHigh]; + const CGFloat kAvatarMinVMargin = 10; + [self.avatarView autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:kAvatarMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [self.avatarView autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:kAvatarMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; self.payloadView = [UIStackView new]; self.payloadView.axis = UILayoutConstraintAxisVertical; @@ -84,10 +93,14 @@ NS_ASSUME_NONNULL_BEGIN [self.payloadView autoPinLeadingToTrailingEdgeOfView:self.avatarView offset:self.avatarHSpacing]; [self.payloadView autoVCenterInSuperview]; // Ensure that the cell's contents never overflow the cell bounds. - // We pin pin to the superview _edge_ and not _margin_ for the purposes + // We pin to the superview _edge_ and not _margin_ for the purposes // of overflow, so that changes to the margins do not trip these safe guards. - [self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; - [self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; + [self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [self.payloadView autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; // We pin the payloadView traillingEdge later, as part of the "Unread Badge" logic. self.nameLabel = [UILabel new]; @@ -147,6 +160,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)configureWithThread:(ThreadViewModel *)thread contactsManager:(OWSContactsManager *)contactsManager blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet +{ + [self configureWithThread:thread + contactsManager:contactsManager + blockedPhoneNumberSet:blockedPhoneNumberSet + overrideSnippet:nil + overrideDate:nil]; +} + +- (void)configureWithThread:(ThreadViewModel *)thread + contactsManager:(OWSContactsManager *)contactsManager + blockedPhoneNumberSet:(NSSet *)blockedPhoneNumberSet + overrideSnippet:(nullable NSAttributedString *)overrideSnippet + overrideDate:(nullable NSDate *)overrideDate { OWSAssertIsOnMainThread(); OWSAssert(thread); @@ -168,13 +194,21 @@ NS_ASSUME_NONNULL_BEGIN self.payloadView.spacing = 0.f; self.topRowView.spacing = self.topRowHSpacing; + if (overrideSnippet) { + self.snippetLabel.attributedText = overrideSnippet; + } else { + self.snippetLabel.attributedText = + [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet]; + } // We update the fonts every time this cell is configured to ensure that // changes to the dynamic type settings are reflected. + // + // Note: we apply this font _after_ we set the attributed text to + // override any font attributes. self.snippetLabel.font = [self snippetFont]; - self.snippetLabel.attributedText = - [self attributedSnippetForThread:thread blockedPhoneNumberSet:blockedPhoneNumberSet]; - self.dateTimeLabel.text = [self stringForDate:thread.lastMessageDate]; + self.dateTimeLabel.text + = (overrideDate ? [self stringForDate:overrideDate] : [self stringForDate:thread.lastMessageDate]); if (hasUnreadMessages) { self.dateTimeLabel.textColor = [UIColor ows_blackColor]; @@ -203,19 +237,22 @@ NS_ASSUME_NONNULL_BEGIN // Spec check. Should be 12pts (6pt on each side) when using default font size. OWSAssert(UIFont.ows_dynamicTypeBodyFont.pointSize != 17 || minMargin == 12); - [self.viewConstraints addObject:[self.unreadBadge autoMatchDimension:ALDimensionWidth - toDimension:ALDimensionWidth - ofView:self.unreadLabel - withOffset:minMargin]]; + [self.viewConstraints addObjectsFromArray:@[ + [self.unreadBadge autoMatchDimension:ALDimensionWidth + toDimension:ALDimensionWidth + ofView:self.unreadLabel + withOffset:minMargin], + // badge sizing + [self.unreadBadge autoSetDimension:ALDimensionWidth + toSize:unreadBadgeHeight + relation:NSLayoutRelationGreaterThanOrEqual], + [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeHeight], + ]]; + }]; + const CGFloat kMinVMargin = 5; [self.viewConstraints addObjectsFromArray:@[ - // badge sizing - [self.unreadBadge autoSetDimension:ALDimensionWidth - toSize:unreadBadgeHeight - relation:NSLayoutRelationGreaterThanOrEqual], - [self.unreadBadge autoSetDimension:ALDimensionHeight toSize:unreadBadgeHeight], - // Horizontally, badge is inserted after the tail of the payloadView, pushing back the date *and* snippet // view [self.payloadView autoPinEdge:ALEdgeTrailing @@ -223,6 +260,12 @@ NS_ASSUME_NONNULL_BEGIN ofView:self.unreadBadge withOffset:-self.topRowHSpacing], [self.unreadBadge autoPinTrailingToSuperviewMargin], + [self.unreadBadge autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual], + [self.unreadBadge autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual], // Vertically, badge is positioned vertically by aligning it's label *subview's* baseline. // This allows us a single visual baseline of text across the top row across [name, dateTime, @@ -367,21 +410,6 @@ NS_ASSUME_NONNULL_BEGIN return minValue * alpha; } -+ (CGFloat)rowHeight -{ - // Scale the cell height using size of dynamic "body" type as a reference. - const CGFloat kReferenceFontSizeMin = 17.f; - const CGFloat kReferenceFontSizeMax = 23.f; - CGFloat referenceFontSize = UIFont.ows_dynamicTypeBodyFont.pointSize; - CGFloat alpha = CGFloatClamp01(CGFloatInverseLerp(referenceFontSize, kReferenceFontSizeMin, kReferenceFontSizeMax)); - - const CGFloat kCellHeightMin = 68.f; - const CGFloat kCellHeightMax = 80.f; - CGFloat result = ceil(CGFloatLerp(kCellHeightMin, kCellHeightMax, alpha)); - - return result; -} - - (NSUInteger)cellHMargin { return 16; diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.h b/Signal/src/ViewControllers/HomeView/HomeViewController.h index d86824b5b..821c34a85 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.h +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.h @@ -6,6 +6,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + @class TSThread; @interface HomeViewController : OWSViewController @@ -25,3 +27,5 @@ animatePresentation:(BOOL)animatePresentation; @end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index fb4d3c048..900a696f5 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -31,6 +31,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + typedef NS_ENUM(NSInteger, HomeViewMode) { HomeViewMode_Archive, HomeViewMode_Inbox, @@ -100,7 +102,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations return self; } -- (instancetype)initWithCoder:(NSCoder *)aDecoder +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { OWSFail(@"Do not load this from the storyboard."); @@ -227,6 +229,8 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.tableView autoPinWidthToSuperview]; [self.tableView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.tableView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:missingContactsPermissionView]; + self.tableView.rowHeight = UITableViewAutomaticDimension; + self.tableView.estimatedRowHeight = 60; UILabel *emptyBoxLabel = [UILabel new]; self.emptyBoxLabel = emptyBoxLabel; @@ -409,8 +413,8 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self presentViewController:navigationController animated:YES completion:nil]; } -- (UIViewController *)previewingContext:(id)previewingContext - viewControllerForLocation:(CGPoint)location +- (nullable UIViewController *)previewingContext:(id)previewingContext + viewControllerForLocation:(CGPoint)location { NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location]; @@ -785,7 +789,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [stackView autoPinEdgeToSuperviewMargin:ALEdgeLeading relation:NSLayoutRelationGreaterThanOrEqual]; [stackView autoPinEdgeToSuperviewMargin:ALEdgeTrailing relation:NSLayoutRelationGreaterThanOrEqual]; // Ensure that the cell's contents never overflow the cell bounds. - // We pin pin to the superview _edge_ and not _margin_ for the purposes + // We pin to the superview _edge_ and not _margin_ for the purposes // of overflow, so that changes to the margins do not trip these safe guards. [stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; [stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0 relation:NSLayoutRelationGreaterThanOrEqual]; @@ -804,11 +808,6 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations return thread; } -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return HomeViewCell.rowHeight; -} - - (void)pullToRefreshPerformed:(UIRefreshControl *)refreshControl { OWSAssertIsOnMainThread(); @@ -828,7 +827,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations return; } -- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath +- (nullable NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath { if ([self isIndexPathForArchivedConversations:indexPath]) { return @[]; @@ -1370,3 +1369,5 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations } @end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/NewContactThreadViewController.m b/Signal/src/ViewControllers/NewContactThreadViewController.m index 886ba2ba8..ed296350d 100644 --- a/Signal/src/ViewControllers/NewContactThreadViewController.m +++ b/Signal/src/ViewControllers/NewContactThreadViewController.m @@ -124,6 +124,9 @@ NS_ASSUME_NONNULL_BEGIN [self.view addSubview:self.tableViewController.view]; [_tableViewController.view autoPinWidthToSuperview]; + self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension; + self.tableViewController.tableView.estimatedRowHeight = 60; + [_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:contactsPermissionReminderView]; [self autoPinViewToBottomOfViewControllerOrKeyboard:self.tableViewController.view]; _tableViewController.tableView.tableHeaderView = searchBar; @@ -429,19 +432,22 @@ NS_ASSUME_NONNULL_BEGIN NSArray *signalAccounts = collatedSignalAccounts[i]; NSMutableArray *contactItems = [NSMutableArray new]; for (SignalAccount *signalAccount in signalAccounts) { - [contactItems addObject:[OWSTableItem itemWithCustomCellBlock:^{ - ContactTableViewCell *cell = [ContactTableViewCell new]; - BOOL isBlocked = [self.contactsViewHelper isRecipientIdBlocked:signalAccount.recipientId]; - if (isBlocked) { - cell.accessoryMessage - = NSLocalizedString(@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked."); - } - - [cell configureWithSignalAccount:signalAccount contactsManager:self.contactsViewHelper.contactsManager]; - - return cell; - } - customRowHeight:[ContactTableViewCell rowHeight] + [contactItems addObject:[OWSTableItem + itemWithCustomCellBlock:^{ + ContactTableViewCell *cell = [ContactTableViewCell new]; + BOOL isBlocked = [self.contactsViewHelper + isRecipientIdBlocked:signalAccount.recipientId]; + if (isBlocked) { + cell.accessoryMessage = NSLocalizedString(@"CONTACT_CELL_IS_BLOCKED", + @"An indicator that a contact has been blocked."); + } + + [cell configureWithSignalAccount:signalAccount + contactsManager:self.contactsViewHelper.contactsManager]; + + return cell; + } + customRowHeight:UITableViewAutomaticDimension actionBlock:^{ [weakSelf newConversationWithRecipientId:signalAccount.recipientId]; }]]; diff --git a/SignalMessaging/ViewControllers/OWSTableViewController.m b/SignalMessaging/ViewControllers/OWSTableViewController.m index 187b9ff76..fe93caee5 100644 --- a/SignalMessaging/ViewControllers/OWSTableViewController.m +++ b/SignalMessaging/ViewControllers/OWSTableViewController.m @@ -112,7 +112,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; actionBlock:(nullable OWSTableActionBlock)actionBlock { OWSAssert(customCell); - OWSAssert(customRowHeight > 0); + OWSAssert(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension); OWSTableItem *item = [OWSTableItem new]; item.actionBlock = actionBlock; @@ -125,7 +125,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; customRowHeight:(CGFloat)customRowHeight actionBlock:(nullable OWSTableActionBlock)actionBlock { - OWSAssert(customRowHeight > 0); + OWSAssert(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension); OWSTableItem *item = [self itemWithCustomCellBlock:customCellBlock actionBlock:actionBlock]; item.customRowHeight = @(customRowHeight); @@ -177,7 +177,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; customRowHeight:(CGFloat)customRowHeight actionBlock:(nullable OWSTableActionBlock)actionBlock { - OWSAssert(customRowHeight > 0); + OWSAssert(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension); OWSTableItem *item = [self disclosureItemWithText:text actionBlock:actionBlock]; item.customRowHeight = @(customRowHeight); @@ -237,7 +237,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; customRowHeight:(CGFloat)customRowHeight actionBlock:(nullable OWSTableSubPageBlock)actionBlock { - OWSAssert(customRowHeight > 0); + OWSAssert(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension); OWSTableItem *item = [self subPageItemWithText:text actionBlock:actionBlock]; item.customRowHeight = @(customRowHeight); @@ -285,7 +285,7 @@ const CGFloat kOWSTable_DefaultCellHeight = 45.f; + (OWSTableItem *)softCenterLabelItemWithText:(NSString *)text customRowHeight:(CGFloat)customRowHeight { - OWSAssert(customRowHeight > 0); + OWSAssert(customRowHeight > 0 || customRowHeight == UITableViewAutomaticDimension); OWSTableItem *item = [self softCenterLabelItemWithText:text]; item.customRowHeight = @(customRowHeight); diff --git a/SignalMessaging/Views/ContactTableViewCell.h b/SignalMessaging/Views/ContactTableViewCell.h index be11ee3ca..b62dcadaf 100644 --- a/SignalMessaging/Views/ContactTableViewCell.h +++ b/SignalMessaging/Views/ContactTableViewCell.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "OWSContactsManager.h" @@ -12,7 +12,6 @@ NS_ASSUME_NONNULL_BEGIN -extern NSString *const kContactsTable_CellReuseIdentifier; extern const NSUInteger kContactTableViewCellAvatarSize; extern const CGFloat kContactTableViewCellAvatarTextMargin; @@ -25,7 +24,7 @@ extern const CGFloat kContactTableViewCellAvatarTextMargin; @property (nonatomic, nullable) NSString *accessoryMessage; @property (nonatomic, readonly) UILabel *subtitle; -+ (nullable NSString *)reuseIdentifier; ++ (NSString *)reuseIdentifier; + (CGFloat)rowHeight; diff --git a/SignalMessaging/Views/ContactTableViewCell.m b/SignalMessaging/Views/ContactTableViewCell.m index eadbc4cb2..06abd423a 100644 --- a/SignalMessaging/Views/ContactTableViewCell.m +++ b/SignalMessaging/Views/ContactTableViewCell.m @@ -18,7 +18,6 @@ NS_ASSUME_NONNULL_BEGIN -NSString *const kContactsTable_CellReuseIdentifier = @"kContactsTable_CellReuseIdentifier"; const NSUInteger kContactTableViewCellAvatarSize = 40; const CGFloat kContactTableViewCellAvatarTextMargin = 12; @@ -37,20 +36,15 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; @implementation ContactTableViewCell -- (instancetype)init +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(nullable NSString *)reuseIdentifier { - if (self = [super init]) { + if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { [self configureProgrammatically]; } return self; } -+ (nullable NSString *)reuseIdentifier -{ - return NSStringFromClass(self.class); -} - -- (nullable NSString *)reuseIdentifier ++ (NSString *)reuseIdentifier { return NSStringFromClass(self.class); } @@ -62,11 +56,15 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; + (CGFloat)rowHeight { - return 59.f; + return 60.f; } - (void)configureProgrammatically { + OWSAssert(!self.nameLabel); + + const CGFloat kMinVMargin = 5; + self.preservesSuperviewLayoutMargins = YES; self.contentView.preservesSuperviewLayoutMargins = YES; @@ -110,6 +108,22 @@ const CGFloat kContactTableViewCellAvatarTextMargin = 12; [_nameContainerView autoPinLeadingToTrailingEdgeOfView:_avatarView offset:kContactTableViewCellAvatarTextMargin]; [_nameContainerView autoPinTrailingToSuperviewMargin]; + // Ensure that the cell's contents never overflow the cell bounds. + // We pin to the superview _edge_ and not _margin_ for the purposes + // of overflow, so that changes to the margins do not trip these safe guards. + [_avatarView autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [_avatarView autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [_nameContainerView autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [_nameContainerView autoPinEdgeToSuperviewEdge:ALEdgeBottom + withInset:kMinVMargin + relation:NSLayoutRelationGreaterThanOrEqual]; + [self configureFonts]; // Force layout, since imageView isn't being initally rendered on App Store optimized build. diff --git a/SignalMessaging/utils/ConversationSearcher.swift b/SignalMessaging/utils/ConversationSearcher.swift index e13156c72..ff72a4d55 100644 --- a/SignalMessaging/utils/ConversationSearcher.swift +++ b/SignalMessaging/utils/ConversationSearcher.swift @@ -9,16 +9,18 @@ public class ConversationSearchResult: Comparable { public let thread: ThreadViewModel public let messageId: String? + public let messageDate: Date? public let snippet: String? private let sortKey: UInt64 - init(thread: ThreadViewModel, messageId: String?, snippet: String?, sortKey: UInt64) { + init(thread: ThreadViewModel, sortKey: UInt64, messageId: String? = nil, messageDate: Date? = nil, snippet: String? = nil) { self.thread = thread + self.sortKey = sortKey self.messageId = messageId + self.messageDate = messageDate self.snippet = snippet - self.sortKey = sortKey } // Mark: Comparable @@ -106,11 +108,11 @@ public class ConversationSearcher: NSObject { var existingConversationRecipientIds: Set = Set() self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in + if let thread = match as? TSThread { let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) - let snippet: String? = thread.lastMessageText(transaction: transaction) let sortKey = NSDate.ows_millisecondsSince1970(for: threadViewModel.lastMessageDate) - let searchResult = ConversationSearchResult(thread: threadViewModel, messageId: nil, snippet: snippet, sortKey: sortKey) + let searchResult = ConversationSearchResult(thread: threadViewModel, sortKey: sortKey) if let contactThread = thread as? TSContactThread { let recipientId = contactThread.contactIdentifier() @@ -123,7 +125,12 @@ public class ConversationSearcher: NSObject { let threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) let sortKey = message.timestamp - let searchResult = ConversationSearchResult(thread: threadViewModel, messageId: message.uniqueId, snippet: snippet, sortKey: sortKey) + let searchResult = ConversationSearchResult(thread: threadViewModel, + sortKey: sortKey, + messageId: message.uniqueId, + messageDate: NSDate.ows_date(withMillisecondsSince1970: message.timestamp), + snippet: snippet) + messages.append(searchResult) } else if let signalAccount = match as? SignalAccount { let searchResult = ContactSearchResult(signalAccount: signalAccount, contactsManager: contactsManager) @@ -187,8 +194,6 @@ public class ConversationSearcher: NSObject { } } - // MARK: - Helpers - // MARK: Searchers private lazy var groupThreadSearcher: Searcher = Searcher { (groupThread: TSGroupThread) in diff --git a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift index d08e2941c..954c4b97d 100644 --- a/SignalServiceKit/src/Storage/FullTextSearchFinder.swift +++ b/SignalServiceKit/src/Storage/FullTextSearchFinder.swift @@ -50,8 +50,10 @@ public class FullTextSearchFinder: NSObject { let maxSearchResults = 500 var searchResultCount = 0 - // (snippet: String, collection: String, key: String, object: Any, stop: UnsafeMutablePointer) - ext.enumerateKeysAndObjects(matching: prefixQuery, with: nil) { (snippet: String, _: String, _: String, object: Any, stop: UnsafeMutablePointer) in + let snippetOptions = YapDatabaseFullTextSearchSnippetOptions() + snippetOptions.startMatchText = "" + snippetOptions.endMatchText = "" + ext.enumerateKeysAndObjects(matching: prefixQuery, with: snippetOptions) { (snippet: String, _: String, _: String, object: Any, stop: UnsafeMutablePointer) in guard searchResultCount < maxSearchResults else { stop.pointee = true return