From d5944b4b9d8f16bfda3ccf3acd7a8a9b66004aa9 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 19 Feb 2019 09:52:10 -0500 Subject: [PATCH] Add first conversation prompt. --- .../HomeView/HomeViewController.m | 173 ++++++++++++++---- .../translations/en.lproj/Localizable.strings | 30 ++- 2 files changed, 147 insertions(+), 56 deletions(-) diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index fff3554a9..ad56ac345 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -71,7 +71,9 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations @property (nonatomic) UITableView *tableView; @property (nonatomic) UIView *emptyInboxView; + @property (nonatomic) UIView *firstConversationCueView; +@property (nonatomic) UILabel *firstConversationLabel; @property (nonatomic) YapDatabaseConnection *editingDbConnection; @property (nonatomic) YapDatabaseConnection *uiDatabaseConnection; @@ -218,6 +220,10 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations OWSAssertIsOnMainThread(); [self reloadTableViewData]; + + if (!self.firstConversationCueView.isHidden) { + [self updateFirstConversationLabel]; + } } - (void)registrationStateDidChange:(id)notification @@ -336,12 +342,15 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations [self.emptyInboxView autoPinWidthToSuperviewMargins]; [self.emptyInboxView autoVCenterInSuperview]; - self.firstConversationCueView = [self createFirstConversationCueView]; + [self createFirstConversationCueView]; [self.view addSubview:self.firstConversationCueView]; - [self.firstConversationCueView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [self.firstConversationCueView autoPinEdgeToSuperviewMargin:ALEdgeTrailing]; - [self.emptyInboxView autoPinWidthToSuperviewMargins]; - [self.emptyInboxView autoVCenterInSuperview]; + [self.firstConversationCueView autoPinToTopLayoutGuideOfViewController:self withInset:0.f]; + [self.firstConversationCueView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10]; + [self.firstConversationCueView autoPinEdgeToSuperviewEdge:ALEdgeLeading + withInset:10 + relation:NSLayoutRelationGreaterThanOrEqual]; + [self.firstConversationCueView autoPinEdgeToSuperviewMargin:ALEdgeBottom + relation:NSLayoutRelationGreaterThanOrEqual]; UIRefreshControl *pullToRefreshView = [UIRefreshControl new]; pullToRefreshView.tintColor = [UIColor grayColor]; @@ -370,7 +379,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations UILabel *emptyInboxLabel = [UILabel new]; emptyInboxLabel.text = NSLocalizedString(@"INBOX_VIEW_EMPTY_INBOX", @"Message shown in the home view when the inbox is empty."); - emptyInboxLabel.font = UIFont.ows_dynamicTypeBodyFont; + emptyInboxLabel.font = UIFont.ows_dynamicTypeBodyClampedFont; emptyInboxLabel.textColor = Theme.secondaryColor; emptyInboxLabel.textAlignment = NSTextAlignmentCenter; emptyInboxLabel.numberOfLines = 0; @@ -388,41 +397,128 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations return emptyInboxStack; } -- (UIView *)createFirstConversationCueView +- (void)createFirstConversationCueView { - NSArray *emptyInboxImageNames = @[ - @"home_empty_splash_1", - @"home_empty_splash_2", - @"home_empty_splash_3", - @"home_empty_splash_4", - @"home_empty_splash_5", - ]; - NSString *emptyInboxImageName = emptyInboxImageNames[arc4random_uniform((uint32_t)emptyInboxImageNames.count)]; - UIImageView *emptyInboxImageView = [UIImageView new]; - emptyInboxImageView.image = [UIImage imageNamed:emptyInboxImageName]; - emptyInboxImageView.layer.minificationFilter = kCAFilterTrilinear; - emptyInboxImageView.layer.magnificationFilter = kCAFilterTrilinear; - [emptyInboxImageView autoPinToAspectRatioWithSize:emptyInboxImageView.image.size]; + const CGFloat kTailWidth = 16.f; + const CGFloat kTailHeight = 8.f; + const CGFloat kTailHMargin = 12.f; - UILabel *emptyInboxLabel = [UILabel new]; - emptyInboxLabel.text - = NSLocalizedString(@"INBOX_VIEW_EMPTY_INBOX", @"Message shown in the home view when the inbox is empty."); - emptyInboxLabel.font = UIFont.ows_dynamicTypeBodyFont; - emptyInboxLabel.textColor = Theme.secondaryColor; - emptyInboxLabel.textAlignment = NSTextAlignmentCenter; - emptyInboxLabel.numberOfLines = 0; - emptyInboxLabel.lineBreakMode = NSLineBreakByWordWrapping; + UILabel *label = [UILabel new]; + label.textColor = UIColor.ows_whiteColor; + label.font = UIFont.ows_dynamicTypeBodyClampedFont; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + + OWSLayerView *layerView = [OWSLayerView new]; + layerView.layoutMargins = UIEdgeInsetsMake(11 + kTailHeight, 16, 7, 16); + CAShapeLayer *shapeLayer = [CAShapeLayer new]; + shapeLayer.fillColor = [OWSConversationColor ows_wintergreenColor].CGColor; + [layerView.layer addSublayer:shapeLayer]; + layerView.layoutCallback = ^(UIView *view) { + UIBezierPath *bezierPath = [UIBezierPath new]; + + // Bubble + CGRect bubbleBounds = view.bounds; + bubbleBounds.origin.y += kTailHeight; + bubbleBounds.size.height -= kTailHeight; + [bezierPath appendPath:[UIBezierPath bezierPathWithRoundedRect:bubbleBounds cornerRadius:8]]; + + // Tail + CGPoint tailTop = CGPointMake(kTailHMargin + kTailWidth * 0.5f, 0.f); + CGPoint tailLeft = CGPointMake(kTailHMargin, kTailHeight); + CGPoint tailRight = CGPointMake(kTailHMargin + kTailWidth, kTailHeight); + if (!CurrentAppContext().isRTL) { + tailTop.x = view.width - tailTop.x; + tailLeft.x = view.width - tailLeft.x; + tailRight.x = view.width - tailRight.x; + } + [bezierPath moveToPoint:tailTop]; + [bezierPath addLineToPoint:tailLeft]; + [bezierPath addLineToPoint:tailRight]; + [bezierPath addLineToPoint:tailTop]; + shapeLayer.path = bezierPath.CGPath; + shapeLayer.frame = view.bounds; + }; + + [layerView addSubview:label]; + [label ows_autoPinToSuperviewMargins]; + + self.firstConversationCueView = layerView; + self.firstConversationLabel = label; +} + +- (void)updateFirstConversationLabel +{ + NSArray *signalAccounts = self.contactsManager.signalAccounts; + + NSString *formatString = @""; + NSMutableArray *contactNames = [NSMutableArray new]; + if (signalAccounts.count >= 3) { + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]]; + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[2]]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have at least " + @"3 Signal contacts. Embeds {{The names of 3 of your Signal contacts}}."); + } else if (signalAccounts.count >= 2) { + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal " + @"contacts. Embeds {{The names of 2 of your Signal contacts}}."); + } else if (signalAccounts.count >= 3) { + [contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]]; + + formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT", + @"Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal " + @"contact. Embeds {{The name of 1 of your Signal contacts}}."); + } - UIStackView *emptyInboxStack = [[UIStackView alloc] initWithArrangedSubviews:@[ - emptyInboxImageView, - emptyInboxLabel, - ]]; - emptyInboxStack.axis = UILayoutConstraintAxisVertical; - emptyInboxStack.alignment = UIStackViewAlignmentCenter; - emptyInboxStack.spacing = 12; - emptyInboxStack.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50); - emptyInboxStack.layoutMarginsRelativeArrangement = YES; - return emptyInboxStack; + NSString *embedToken = @"%@"; + NSArray *formatSplits = [formatString componentsSeparatedByString:embedToken]; + // We need to use a complicated format string that possibly embeds multiple contact names. + // Translator error could easily lead to an invalid format string. + // We need to verify that it was translated properly. + BOOL isValidFormatString = (contactNames.count > 0 && formatSplits.count == contactNames.count + 1); + for (NSString *contactName in contactNames) { + if ([contactName containsString:embedToken]) { + isValidFormatString = NO; + } + } + + NSMutableAttributedString *_Nullable attributedString = nil; + if (isValidFormatString) { + attributedString = [[NSMutableAttributedString alloc] initWithString:formatString]; + while (contactNames.count > 0) { + NSString *contactName = contactNames.firstObject; + [contactNames removeObjectAtIndex:0]; + + NSRange range = [attributedString.string rangeOfString:embedToken]; + if (range.location == NSNotFound) { + // Error + attributedString = nil; + break; + } + + NSAttributedString *formattedName = [[NSAttributedString alloc] + initWithString:contactName + attributes:@{ + NSFontAttributeName : self.firstConversationLabel.font.ows_mediumWeight, + }]; + [attributedString replaceCharactersInRange:range withAttributedString:formattedName]; + } + } + + if (!attributedString) { + // The default case handles the no-contacts scenario and all error cases. + NSString *defaultText = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_NO_CONTACTS", + @"A label offering to start a new conversation with your contacts, if you have no Signal contacts."); + attributedString = [[NSMutableAttributedString alloc] initWithString:defaultText]; + } + + self.firstConversationLabel.attributedText = [attributedString copy]; } - (void)updateReminderViews @@ -1524,6 +1620,7 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations if (self.homeViewMode == HomeViewMode_Inbox && inboxCount == 0 && archiveCount == 0) { [_tableView setHidden:YES]; [self.emptyInboxView setHidden:NO]; + [self updateFirstConversationLabel]; } else { [_tableView setHidden:NO]; [self.emptyInboxView setHidden:YES]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 01dc21dc7..7f5a585c4 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -819,30 +819,12 @@ /* subject of email sent to contacts when inviting to install Signal */ "EMAIL_INVITE_SUBJECT" = "Let's switch to Signal"; -/* Body text an existing user sees when viewing an empty archive */ -"EMPTY_ARCHIVE_TEXT" = "You can archive inactive conversations from your Inbox."; - -/* Header text an existing user sees when viewing an empty archive */ -"EMPTY_ARCHIVE_TITLE" = "Clean Up Your Conversation List"; - /* Full width label displayed when attempting to compose message */ "EMPTY_CONTACTS_LABEL_LINE1" = "None of your contacts have Signal."; /* Full width label displayed when attempting to compose message */ "EMPTY_CONTACTS_LABEL_LINE2" = "Why don't you invite someone?"; -/* Body text a new user sees when viewing an empty inbox */ -"EMPTY_INBOX_NEW_USER_TEXT" = "Tap on the compose button"; - -/* Header text a new user sees when viewing an empty inbox */ -"EMPTY_INBOX_NEW_USER_TITLE" = "Start your first Signal conversation!"; - -/* Body text an existing user sees when viewing an empty inbox */ -"EMPTY_INBOX_TEXT" = "None. Zip. Zilch. Nada."; - -/* Header text an existing user sees when viewing an empty inbox */ -"EMPTY_INBOX_TITLE" = "Inbox Zero"; - /* Indicates that user should confirm their 'two factor auth pin'. */ "ENABLE_2FA_VIEW_CONFIRM_PIN_INSTRUCTIONS" = "Confirm your PIN."; @@ -1074,6 +1056,18 @@ /* Placeholder text for search bar which filters conversations. */ "HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER" = "Search"; +/* Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal contact. Embeds: {{The name of 1 of your Signal contacts}}. */ +"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT" = "Some of your contacts are already on Signal, including %@."; + +/* Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal contacts. Embeds: {{The names of 2 of your Signal contacts}}. */ +"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT" = "Some of your contacts are already on Signal, including %@ and %@"; + +/* Format string for a label offering to start a new conversation with your contacts, if you have at least 3 Signal contacts. Embeds: {{The names of 3 of your Signal contacts}}. */ +"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT" = "Some of your contacts are already on Signal, including %@, %@ and %@"; + +/* A label offering to start a new conversation with your contacts, if you have no Signal contacts. */ +"HOME_VIEW_FIRST_CONVERSATION_OFFER_NO_CONTACTS" = "Start your first conversation here."; + /* Format string when search returns no results. Embeds {{search term}} */ "HOME_VIEW_SEARCH_NO_RESULTS_FORMAT" = "No results found for '%@'";