diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index b344b871a6..cb3f7062db 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -882,6 +882,8 @@ typedef enum : NSUInteger { return; } + NSMutableArray *banners = [NSMutableArray new]; + NSArray *noLongerVerifiedAddresses = [self noLongerVerifiedAddresses]; if (noLongerVerifiedAddresses.count > 0) { @@ -902,10 +904,9 @@ typedef enum : NSUInteger { message = [NSString stringWithFormat:format, displayName]; } - [self createBannerWithTitle:message - bannerColor:UIColor.ows_accentRedColor - tapSelector:@selector(noLongerVerifiedBannerViewWasTapped:)]; - return; + [banners addObject:[self createBannerWithTitle:message + bannerColor:UIColor.ows_accentRedColor + tapSelector:@selector(noLongerVerifiedBannerViewWasTapped:)]]; } NSString *blockStateMessage = nil; @@ -924,34 +925,64 @@ typedef enum : NSUInteger { } if (blockStateMessage) { - [self createBannerWithTitle:blockStateMessage - bannerColor:UIColor.ows_accentRedColor - tapSelector:@selector(blockBannerViewWasTapped:)]; + [banners addObject:[self createBannerWithTitle:blockStateMessage + bannerColor:UIColor.ows_accentRedColor + tapSelector:@selector(blockBannerViewWasTapped:)]]; + } + + NSUInteger pendingMemberRequestCount = self.pendingMemberRequestCount; + if (pendingMemberRequestCount > 0) { + [banners addObject:[self createPendingJoinReuqestBannerWithCount:pendingMemberRequestCount]]; + } + + if (banners.count < 1) { return; } + + UIStackView *bannerView = [[UIStackView alloc] initWithArrangedSubviews:banners]; + bannerView.axis = UILayoutConstraintAxisVertical; + bannerView.alignment = UIStackViewAlignmentFill; + [self.view addSubview:bannerView]; + [bannerView autoPinToTopLayoutGuideOfViewController:self withInset:0]; + [bannerView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; + [bannerView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; + + UIView *bannerShadow = [UIView new]; + bannerShadow.backgroundColor = Theme.backgroundColor; + // Use a shadow to "pop" the indicator above the other views. + bannerShadow.layer.shadowColor = [UIColor blackColor].CGColor; + bannerShadow.layer.shadowOffset = CGSizeMake(0, 4); + bannerShadow.layer.shadowRadius = 4.f; + bannerShadow.layer.shadowOpacity = 0.15f; + [bannerShadow addRedBorder]; + [bannerView addSubview:bannerShadow]; + [bannerShadow autoPinEdgesToSuperviewEdges]; + [bannerView sendSubviewToBack:bannerShadow]; + + [self.view layoutSubviews]; + + self.bannerView = bannerView; +} + +- (NSUInteger)pendingMemberRequestCount +{ + if ([self.thread isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *groupThread = (TSGroupThread *)self.thread; + return groupThread.groupMembership.requestingMembers.count; + } else { + return 0; + } } -- (void)createBannerWithTitle:(NSString *)title bannerColor:(UIColor *)bannerColor tapSelector:(SEL)tapSelector +- (UIView *)createBannerWithTitle:(NSString *)title bannerColor:(UIColor *)bannerColor tapSelector:(SEL)tapSelector { OWSAssertDebug(title.length > 0); OWSAssertDebug(bannerColor); UIView *bannerView = [UIView containerView]; bannerView.backgroundColor = bannerColor; - bannerView.layer.cornerRadius = 2.5f; - // Use a shadow to "pop" the indicator above the other views. - bannerView.layer.shadowColor = [UIColor blackColor].CGColor; - bannerView.layer.shadowOffset = CGSizeMake(2, 3); - bannerView.layer.shadowRadius = 2.f; - bannerView.layer.shadowOpacity = 0.35f; - - UILabel *label = [UILabel new]; - label.font = [UIFont ows_semiboldFontWithSize:14.f]; - label.text = title; - label.textColor = [UIColor whiteColor]; - label.numberOfLines = 0; - label.lineBreakMode = NSLineBreakByWordWrapping; + UILabel *label = [self buildBannerLabel:title]; label.textAlignment = NSTextAlignmentCenter; UIImage *closeIcon = [UIImage imageNamed:@"banner_close"]; @@ -974,22 +1005,71 @@ typedef enum : NSUInteger { [bannerView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:tapSelector]]; bannerView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"banner_close"); - [self.view addSubview:bannerView]; - [bannerView autoPinToTopLayoutGuideOfViewController:self withInset:10]; - [bannerView autoHCenterInSuperview]; + return bannerView; +} - CGFloat labelDesiredWidth = [label sizeThatFits:CGSizeZero].width; - CGFloat bannerDesiredWidth - = (labelDesiredWidth + kBannerHPadding + kBannerHSpacing + closeIcon.size.width + kBannerCloseButtonPadding); - const CGFloat kMinBannerHMargin = 20.f; - if (bannerDesiredWidth + kMinBannerHMargin * 2.f >= self.view.width) { - [bannerView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:kMinBannerHMargin]; - [bannerView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:kMinBannerHMargin]; - } +- (UILabel *)buildBannerLabel:(NSString *)title +{ + UILabel *label = [UILabel new]; + label.font = [UIFont ows_dynamicTypeSubheadlineClampedFont].ows_semibold; + label.text = title; + label.textColor = [UIColor whiteColor]; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + return label; +} - [self.view layoutSubviews]; +- (UIView *)createPendingJoinReuqestBannerWithCount:(NSUInteger)pendingMemberRequestCount +{ + OWSAssertDebug(pendingMemberRequestCount > 0); - self.bannerView = bannerView; + NSString *format = NSLocalizedString(@"PENDING_GROUP_MEMBERS_REQUEST_BANNER_FORMAT", + @"Format for banner indicating that there are pending member requests to join the group. Embeds {{ the number " + @"of pending member requests }}."); + NSString *title = [NSString stringWithFormat:format, [OWSFormat formatUInt:pendingMemberRequestCount]]; + UILabel *label = [self buildBannerLabel:title]; + label.font = [UIFont ows_dynamicTypeSubheadlineClampedFont]; + + __weak ConversationViewController *weakSelf = self; + OWSButton *dismissButton = [[OWSButton alloc] initWithTitle:CommonStrings.dismissButton + block:^{ [weakSelf hidePendingMemberRequests]; }]; + dismissButton.titleLabel.font = [UIFont ows_dynamicTypeSubheadlineClampedFont].ows_semibold; + NSString *viewRequestsLabel = NSLocalizedString(@"PENDING_GROUP_MEMBERS_REQUEST_BANNER_VIEW_REQUESTS", + @"Label for the 'view requests' button in the pending member requests banner."); + OWSButton *viewRequestsButton = [[OWSButton alloc] initWithTitle:viewRequestsLabel + block:^{ [weakSelf viewMemberRequests]; }]; + viewRequestsButton.titleLabel.font = [UIFont ows_dynamicTypeSubheadlineClampedFont].ows_semibold; + + UIStackView *buttonRow = [[UIStackView alloc] initWithArrangedSubviews:@[ + [UIView hStretchingSpacer], + dismissButton, + viewRequestsButton, + ]]; + buttonRow.axis = UILayoutConstraintAxisHorizontal; + buttonRow.spacing = 24; + + UIStackView *bannerView = [[UIStackView alloc] initWithArrangedSubviews:@[ + label, + buttonRow, + ]]; + bannerView.axis = UILayoutConstraintAxisVertical; + bannerView.alignment = UIStackViewAlignmentFill; + bannerView.spacing = 10; + bannerView.layoutMargins = UIEdgeInsetsMake(14, 16, 14, 16); + [bannerView setLayoutMarginsRelativeArrangement:YES]; + [bannerView addBackgroundViewWithBackgroundColor:UIColor.ows_accentBlueColor]; + bannerView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"pending_group_request_banner"); + + return bannerView; +} + +- (void)hidePendingMemberRequests +{ +} + +- (void)viewMemberRequests +{ + [self showConversationSettingsAndShowMemberRequests]; } - (void)blockBannerViewWasTapped:(UIGestureRecognizer *)sender @@ -1591,7 +1671,7 @@ typedef enum : NSUInteger { { NSArray *noLongerVerifiedAddresses = [self noLongerVerifiedAddresses]; if (noLongerVerifiedAddresses.count > 1) { - [self showConversationSettingsAndShowVerification:YES]; + [self showConversationSettingsAndShowVerification]; } else if (noLongerVerifiedAddresses.count == 1) { // Pick one in an arbitrary but deterministic manner. SignalServiceAddress *address = noLongerVerifiedAddresses.lastObject; @@ -1601,34 +1681,52 @@ typedef enum : NSUInteger { - (void)showConversationSettings { - [self showConversationSettingsAndShowVerification:NO]; -} - -- (UIViewController *)buildConversationSettingsView:(BOOL)showVerificationOnAppear -{ - ConversationSettingsViewController *settingsVC = - [[ConversationSettingsViewController alloc] initWithThreadViewModel:self.threadViewModel]; - settingsVC.conversationSettingsViewDelegate = self; - return settingsVC; -} - -- (void)showConversationSettingsAndShowVerification:(BOOL)showVerification -{ - UIViewController *settingsVC = [self buildConversationSettingsView:showVerification]; - - [self.navigationController setViewControllers:[self.viewControllersUpToSelf arrayByAddingObject:settingsVC] - animated:YES]; + [self showConversationSettingsWithMode:ConversationSettingsPresentationModeDefault]; } - (void)showConversationSettingsAndShowAllMedia { - UIViewController *settingsVC = [self buildConversationSettingsView:NO]; + [self showConversationSettingsWithMode:ConversationSettingsPresentationModeShowAllMedia]; +} - MediaTileViewController *allMedia = [[MediaTileViewController alloc] initWithThread:self.thread]; +- (void)showConversationSettingsAndShowVerification +{ + [self showConversationSettingsWithMode:ConversationSettingsPresentationModeShowVerification]; +} - [self.navigationController - setViewControllers:[self.viewControllersUpToSelf arrayByAddingObjectsFromArray:@[ settingsVC, allMedia ]] - animated:YES]; +- (void)showConversationSettingsAndShowMemberRequests +{ + [self showConversationSettingsWithMode:ConversationSettingsPresentationModeShowMemberRequests]; +} + +- (void)showConversationSettingsWithMode:(ConversationSettingsPresentationMode)mode +{ + NSMutableArray *viewControllers = [self.viewControllersUpToSelf mutableCopy]; + + ConversationSettingsViewController *settingsView = + [[ConversationSettingsViewController alloc] initWithThreadViewModel:self.threadViewModel]; + settingsView.conversationSettingsViewDelegate = self; + [viewControllers addObject:settingsView]; + + switch (mode) { + case ConversationSettingsPresentationModeDefault: + break; + case ConversationSettingsPresentationModeShowVerification: + settingsView.showVerificationOnAppear = YES; + break; + case ConversationSettingsPresentationModeShowMemberRequests: { + UIViewController *_Nullable view = [settingsView buildMemberRequestsAndInvitesView]; + if (view != nil) { + [viewControllers addObject:view]; + } + break; + } + case ConversationSettingsPresentationModeShowAllMedia: + [viewControllers addObject:[[MediaTileViewController alloc] initWithThread:self.thread]]; + break; + } + + [self.navigationController setViewControllers:viewControllers animated:YES]; } - (NSArray *)viewControllersUpToSelf @@ -4803,6 +4901,7 @@ typedef enum : NSUInteger { // viewWillAppear will call resetContentAndLayout. return; } + NSUInteger oldPendingMemberRequestCount = self.pendingMemberRequestCount; TSThread *_Nullable lastestThread = [TSThread anyFetchWithUniqueId:self.thread.uniqueId transaction:transaction]; if (lastestThread == nil) { lastestThread = self.thread; @@ -4811,6 +4910,11 @@ typedef enum : NSUInteger { [self updateNavigationBarSubtitleLabel]; [self updateBarButtonItems]; + NSUInteger newPendingMemberRequestCount = self.pendingMemberRequestCount; + if (oldPendingMemberRequestCount != newPendingMemberRequestCount) { + [self ensureBannerState]; + } + // If the message has been deleted / disappeared, we need to dismiss [self dismissMessageActionsIfNecessary]; diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift index 261ca8d5f3..8aebdc5204 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationSettingsViewController.swift @@ -7,6 +7,16 @@ import PromiseKit import UIKit import ContactsUI +@objc +public enum ConversationSettingsPresentationMode: UInt { + case `default` + case showVerification + case showMemberRequests + case showAllMedia +} + +// MARK: - + @objc public protocol ConversationSettingsViewDelegate: class { @@ -78,6 +88,7 @@ class ConversationSettingsViewController: OWSTableViewController { // This is updated as we change group membership, etc. var currentGroupModel: TSGroupModel? + @objc public var showVerificationOnAppear = false var disappearingMessagesConfiguration: OWSDisappearingMessagesConfiguration! @@ -577,14 +588,23 @@ class ConversationSettingsViewController: OWSTableViewController { } func showMemberRequestsAndInvitesView() { - guard let groupThread = thread as? TSGroupThread else { + guard let viewController = buildMemberRequestsAndInvitesView() else { owsFailDebug("Invalid thread.") return } + navigationController?.pushViewController(viewController, animated: true) + } + + @objc + public func buildMemberRequestsAndInvitesView() -> UIViewController? { + guard let groupThread = thread as? TSGroupThread else { + owsFailDebug("Invalid thread.") + return nil + } let groupMemberRequestsAndInvitesViewController = GroupMemberRequestsAndInvitesViewController(groupThread: groupThread, groupViewHelper: groupViewHelper) groupMemberRequestsAndInvitesViewController.groupMemberRequestsAndInvitesViewControllerDelegate = self - navigationController?.pushViewController(groupMemberRequestsAndInvitesViewController, animated: true) + return groupMemberRequestsAndInvitesViewController } func showGroupLinkView() { diff --git a/Signal/src/ViewControllers/ThreadSettings/GroupLinkQRCodeViewController.swift b/Signal/src/ViewControllers/ThreadSettings/GroupLinkQRCodeViewController.swift index 411c2a206c..6a2d891cfe 100644 --- a/Signal/src/ViewControllers/ThreadSettings/GroupLinkQRCodeViewController.swift +++ b/Signal/src/ViewControllers/ThreadSettings/GroupLinkQRCodeViewController.swift @@ -32,10 +32,7 @@ public class GroupLinkQRCodeViewController: OWSViewController { private func createContents() { - let qrCodeView = QRCodeView(useCircularWrapper: true) - let qrCodeViewWrapper = UIStackView(arrangedSubviews: [qrCodeView]) - qrCodeViewWrapper.layoutMargins = UIEdgeInsets(top: 0, leading: 40, bottom: 0, trailing: 40) - qrCodeViewWrapper.isLayoutMarginsRelativeArrangement = true + let qrCodeView = QRCodeView() do { let inviteLinkUrl = try GroupManager.groupInviteLink(forGroupModelV2: groupModelV2) @@ -66,7 +63,7 @@ public class GroupLinkQRCodeViewController: OWSViewController { let vSpacer2 = UIView.vStretchingSpacer() let stackView = UIStackView(arrangedSubviews: [ vSpacer1, - qrCodeViewWrapper, + qrCodeView, UIView.spacer(withHeight: 24), descriptionLabel, vSpacer2, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 92e5a30fd4..06039578b0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2893,6 +2893,12 @@ /* Message indicating that a request to join the group was successfully approved. Embeds {{ the name of the approved user }}. */ "PENDING_GROUP_MEMBERS_REQUEST_APPROVED_FORMAT" = "Added “%@“."; +/* Format for banner indicating that there are pending member requests to join the group. Embeds {{ the number of pending member requests }}. */ +"PENDING_GROUP_MEMBERS_REQUEST_BANNER_FORMAT" = "%@ pending member requests."; + +/* Label for the 'view requests' button in the pending member requests banner. */ +"PENDING_GROUP_MEMBERS_REQUEST_BANNER_VIEW_REQUESTS" = "View Requests"; + /* Message indicating that a request to join the group was successfully denied. Embeds {{ the name of the denied user }}. */ "PENDING_GROUP_MEMBERS_REQUEST_DENIED_FORMAT" = "Denied “%@“.";