// // Copyright (c) 2020 Open Whisper Systems. All rights reserved. // #import "OWSConversationSettingsViewController.h" #import "BlockListUIUtils.h" #import "ContactsViewHelper.h" #import "FingerprintViewController.h" #import "OWSAddToContactViewController.h" #import "OWSBlockingManager.h" #import "OWSSoundSettingsViewController.h" #import "PhoneNumber.h" #import "ShowGroupMembersViewController.h" #import "Signal-Swift.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import "UpdateGroupViewController.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN //#define SHOW_COLOR_PICKER const CGFloat kIconViewLength = 24; @interface OWSConversationSettingsViewController () @property (nonatomic, readonly) TSThread *thread; @property (nonatomic, readonly) ThreadViewModel *threadViewModel; @property (nonatomic) NSArray *disappearingMessagesDurations; @property (nonatomic) OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @property (nonatomic, readonly) UIImageView *avatarView; @property (nonatomic, readonly) UILabel *disappearingMessagesDurationLabel; #ifdef SHOW_COLOR_PICKER @property (nonatomic) OWSColorPicker *colorPicker; #endif @end #pragma mark - @implementation OWSConversationSettingsViewController - (instancetype)init { self = [super init]; if (!self) { return self; } _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; [self observeNotifications]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Dependencies - (MessageSenderJobQueue *)messageSenderJobQueue { return SSKEnvironment.shared.messageSenderJobQueue; } - (TSAccountManager *)tsAccountManager { OWSAssertDebug(SSKEnvironment.shared.tsAccountManager); return SSKEnvironment.shared.tsAccountManager; } - (OWSContactsManager *)contactsManager { return Environment.shared.contactsManager; } - (OWSMessageSender *)messageSender { return SSKEnvironment.shared.messageSender; } - (OWSBlockingManager *)blockingManager { return [OWSBlockingManager sharedManager]; } - (OWSProfileManager *)profileManager { return [OWSProfileManager sharedManager]; } - (SDSDatabaseStorage *)databaseStorage { return SDSDatabaseStorage.shared; } #pragma mark - (void)observeNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(identityStateDidChange:) name:kNSNotificationNameIdentityStateDidChange object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherUsersProfileDidChange:) name:kNSNotificationNameOtherUsersProfileDidChange object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(profileWhitelistDidChange:) name:kNSNotificationNameProfileWhitelistDidChange object:nil]; } - (NSString *)threadName { NSString *threadName = [self.contactsManager displayNameForThreadWithSneakyTransaction:self.thread]; if (![self.thread isKindOfClass:[TSContactThread class]]) { return threadName; } TSContactThread *contactThread = (TSContactThread *)self.thread; NSString *_Nullable phoneNumber = contactThread.contactAddress.phoneNumber; if (phoneNumber && [threadName isEqualToString:phoneNumber]) { threadName = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:phoneNumber]; } return threadName; } - (BOOL)canEditSharedConversationSettings { if (self.threadViewModel.hasPendingMessageRequest) { return NO; } return self.isLocalUserInConversation; } - (BOOL)isLocalUserInConversation { if (!self.isGroupThread) { return YES; } TSGroupThread *groupThread = (TSGroupThread *)self.thread; return groupThread.isLocalUserInGroup; } - (BOOL)isGroupThread { return [self.thread isKindOfClass:[TSGroupThread class]]; } - (BOOL)hasSavedGroupIcon { if (![self isGroupThread]) { return NO; } TSGroupThread *groupThread = (TSGroupThread *)self.thread; return groupThread.groupModel.groupAvatarData.length > 0; } - (void)configureWithThreadViewModel:(ThreadViewModel *)threadViewModel { OWSAssertDebug(threadViewModel); _threadViewModel = threadViewModel; if ([self.thread isKindOfClass:[TSContactThread class]]) { self.title = NSLocalizedString( @"CONVERSATION_SETTINGS_CONTACT_INFO_TITLE", @"Navbar title when viewing settings for a 1-on-1 thread"); } else { self.title = NSLocalizedString( @"CONVERSATION_SETTINGS_GROUP_INFO_TITLE", @"Navbar title when viewing settings for a group thread"); } // This will only appear in internal, qa & dev builds. if (SSKDebugFlags.groupsV2showV2Indicator) { NSString *indicator = self.thread.isGroupV2Thread ? @"v2" : @"v1"; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:LocalizationNotNeeded(indicator) style:UIBarButtonItemStylePlain target:nil action:nil]; } } - (TSThread *)thread { return self.threadViewModel.threadRecord; } - (BOOL)hasExistingContact { OWSAssertDebug([self.thread isKindOfClass:[TSContactThread class]]); TSContactThread *contactThread = (TSContactThread *)self.thread; SignalServiceAddress *recipientAddress = contactThread.contactAddress; return [self.contactsManager hasSignalAccountForAddress:recipientAddress]; } #pragma mark - CNContactViewControllerDelegate - (void)contactViewController:(CNContactViewController *)viewController didCompleteWithContact:(nullable CNContact *)contact { [self updateTableContents]; [self.navigationController popToViewController:self animated:YES]; } #pragma mark - ContactsViewHelperDelegate - (void)contactsViewHelperDidUpdateContacts { [self updateTableContents]; } #pragma mark - View Lifecycle - (void)viewDidLoad { [super viewDidLoad]; self.useThemeBackgroundColors = YES; self.tableView.estimatedRowHeight = 45; self.tableView.rowHeight = UITableViewAutomaticDimension; _disappearingMessagesDurationLabel = [UILabel new]; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _disappearingMessagesDurationLabel); self.disappearingMessagesDurations = [OWSDisappearingMessagesConfiguration validDurationsSeconds]; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *transaction) { self.disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration fetchOrBuildDefaultWithThread:self.thread transaction:transaction]; }]; #ifdef SHOW_COLOR_PICKER self.colorPicker = [[OWSColorPicker alloc] initWithThread:self.thread]; self.colorPicker.delegate = self; #endif [self updateTableContents]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; if (self.showVerificationOnAppear) { self.showVerificationOnAppear = NO; if (self.isGroupThread) { [self showGroupMembersView]; } else { [self showVerificationView]; } } } - (void)updateTableContents { OWSTableContents *contents = [OWSTableContents new]; contents.title = NSLocalizedString(@"CONVERSATION_SETTINGS", @"title for conversation settings screen"); BOOL isNoteToSelf = self.thread.isNoteToSelf; BOOL canEditSharedConversationSettings = self.canEditSharedConversationSettings; BOOL isLocalUserInConversation = self.isLocalUserInConversation; __weak OWSConversationSettingsViewController *weakSelf = self; // Main section. OWSTableSection *mainSection = [OWSTableSection new]; mainSection.customHeaderView = [self mainSectionHeader]; mainSection.customHeaderHeight = @(100.f); if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing && !self.hasExistingContact) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ return [weakSelf disclosureCellWithName: NSLocalizedString(@"CONVERSATION_SETTINGS_ADD_TO_SYSTEM_CONTACTS", @"button in conversation settings view.") icon:ThemeIconSettingsAddToContacts accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"add_to_system_contacts")]; } actionBlock:^{ ActionSheetController *actionSheet = [[ActionSheetController alloc] initWithTitle:nil message:nil]; NSString *createNewTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_NEW_CONTACT", @"Label for 'new contact' button in conversation settings view."); [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:createNewTitle style:ActionSheetActionStyleDefault handler:^(ActionSheetAction *_Nonnull action) { OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); [strongSelf presentContactViewController]; }]]; NSString *addToExistingTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT", @"Label for 'new contact' button in conversation settings view."); [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:addToExistingTitle style:ActionSheetActionStyleDefault handler:^(ActionSheetAction *_Nonnull action) { OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); TSContactThread *contactThread = (TSContactThread *)strongSelf.thread; [strongSelf presentAddToContactViewControllerWithAddress: contactThread.contactAddress]; }]]; [actionSheet addAction:OWSActionSheets.cancelAction]; [self presentActionSheet:actionSheet]; }]]; } [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ return [weakSelf disclosureCellWithName:MediaStrings.allMedia icon:ThemeIconSettingsAllMedia accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"all_media")]; } actionBlock:^{ [weakSelf showMediaGallery]; }]]; [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ NSString *title = NSLocalizedString(@"CONVERSATION_SETTINGS_SEARCH", @"Table cell label in conversation settings which returns the user to the " @"conversation with 'search mode' activated"); return [weakSelf disclosureCellWithName:title icon:ThemeIconSettingsSearch accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"search")]; } actionBlock:^{ [weakSelf tappedConversationSearch]; }]]; if (!isNoteToSelf && !self.isGroupThread && self.thread.hasSafetyNumbers) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ return [weakSelf disclosureCellWithName:NSLocalizedString(@"VERIFY_PRIVACY", @"Label for button or row which allows users to verify the " @"safety number of another user.") icon:ThemeIconSettingsViewSafetyNumber accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"safety_numbers")]; } actionBlock:^{ [weakSelf showVerificationView]; }]]; } // Indicate if the user is in the system contacts if (!isNoteToSelf && !self.isGroupThread && self.hasExistingContact) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); return [strongSelf disclosureCellWithName:NSLocalizedString( @"CONVERSATION_SETTINGS_VIEW_IS_SYSTEM_CONTACT", @"Indicates that user is in the system contacts list.") icon:ThemeIconSettingsUserInContacts accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"is_in_contacts")]; } actionBlock:^{ if (weakSelf.contactsManager.supportsContactEditing) { [weakSelf presentContactViewController]; } }]]; } // Show profile status and allow sharing your profile for threads that are not in the whitelist. // This goes away when phoneNumberPrivacy is enabled, since profile sharing become mandatory. __block BOOL isThreadInProfileWhitelist; __block BOOL hasSentMessages; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *transaction) { isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread transaction:transaction]; hasSentMessages = [[[InteractionFinder alloc] initWithThreadUniqueId:self.thread.uniqueId] existsOutgoingMessageWithTransaction:transaction]; }]; BOOL hideManualProfileSharing = SSKFeatureFlags.phoneNumberPrivacy || (RemoteConfig.messageRequests && isThreadInProfileWhitelist) || (RemoteConfig.messageRequests && !hasSentMessages); BOOL hideProfileShareStatus = SSKFeatureFlags.phoneNumberPrivacy || RemoteConfig.messageRequests; if (hideManualProfileSharing || isNoteToSelf) { // Do nothing } else if (isThreadInProfileWhitelist && !hideProfileShareStatus) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); return [strongSelf labelCellWithName: (strongSelf.isGroupThread ? NSLocalizedString( @"CONVERSATION_SETTINGS_VIEW_PROFILE_IS_SHARED_WITH_GROUP", @"Indicates that user's profile has been shared with a group.") : NSLocalizedString( @"CONVERSATION_SETTINGS_VIEW_PROFILE_IS_SHARED_WITH_USER", @"Indicates that user's profile has been shared with a user.")) icon:ThemeIconSettingsProfile accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"profile_is_shared")]; } actionBlock:nil]]; } else { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); UITableViewCell *cell = [strongSelf disclosureCellWithName: (strongSelf.isGroupThread ? NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE_WITH_GROUP", @"Action that shares user profile with a group.") : NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE_WITH_USER", @"Action that shares user profile with a user.")) icon:ThemeIconSettingsProfile accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"share_profile")]; cell.userInteractionEnabled = isLocalUserInConversation; return cell; } actionBlock:^{ [weakSelf showShareProfileAlert]; }]]; } if (canEditSharedConversationSettings) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [OWSTableItem newCell]; OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); cell.preservesSuperviewLayoutMargins = YES; cell.contentView.preservesSuperviewLayoutMargins = YES; cell.selectionStyle = UITableViewCellSelectionStyleNone; ThemeIcon icon = strongSelf.disappearingMessagesConfiguration.isEnabled ? ThemeIconSettingsTimer : ThemeIconSettingsTimerDisabled; UIImageView *iconView = [strongSelf viewForIcon:icon]; UILabel *rowLabel = [UILabel new]; rowLabel.text = NSLocalizedString( @"DISAPPEARING_MESSAGES", @"table cell label in conversation settings"); rowLabel.textColor = Theme.primaryTextColor; rowLabel.font = OWSTableItem.primaryLabelFont; rowLabel.lineBreakMode = NSLineBreakByTruncatingTail; UISwitch *switchView = [UISwitch new]; switchView.on = strongSelf.disappearingMessagesConfiguration.isEnabled; [switchView addTarget:strongSelf action:@selector(disappearingMessagesSwitchValueDidChange:) forControlEvents:UIControlEventValueChanged]; UIStackView *topRow = [[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel, switchView ]]; topRow.spacing = strongSelf.iconSpacing; topRow.alignment = UIStackViewAlignmentCenter; [cell.contentView addSubview:topRow]; [topRow autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeBottom]; UILabel *subtitleLabel = [UILabel new]; subtitleLabel.text = NSLocalizedString( @"DISAPPEARING_MESSAGES_DESCRIPTION", @"subheading in conversation settings"); subtitleLabel.textColor = Theme.primaryTextColor; subtitleLabel.font = [UIFont ows_dynamicTypeCaption1Font]; subtitleLabel.numberOfLines = 0; subtitleLabel.lineBreakMode = NSLineBreakByWordWrapping; [cell.contentView addSubview:subtitleLabel]; [subtitleLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topRow withOffset:8]; [subtitleLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:rowLabel]; [subtitleLabel autoPinTrailingToSuperviewMargin]; [subtitleLabel autoPinBottomToSuperviewMargin]; switchView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"disappearing_messages_switch"); cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"disappearing_messages"); return cell; } customRowHeight:UITableViewAutomaticDimension actionBlock:nil]]; if (self.disappearingMessagesConfiguration.isEnabled) { [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [OWSTableItem newCell]; OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); cell.preservesSuperviewLayoutMargins = YES; cell.contentView.preservesSuperviewLayoutMargins = YES; cell.selectionStyle = UITableViewCellSelectionStyleNone; UIImageView *iconView = [strongSelf viewForIcon:ThemeIconSettingsTimer]; UILabel *rowLabel = strongSelf.disappearingMessagesDurationLabel; [strongSelf updateDisappearingMessagesDurationLabel]; rowLabel.textColor = Theme.primaryTextColor; rowLabel.font = OWSTableItem.primaryLabelFont; // don't truncate useful duration info which is in the tail rowLabel.lineBreakMode = NSLineBreakByTruncatingHead; UIStackView *topRow = [[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel ]]; topRow.spacing = strongSelf.iconSpacing; topRow.alignment = UIStackViewAlignmentCenter; [cell.contentView addSubview:topRow]; [topRow autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeBottom]; UISlider *slider = [UISlider new]; slider.maximumValue = (float)(strongSelf.disappearingMessagesDurations.count - 1); slider.minimumValue = 0; slider.continuous = YES; // NO fires change event only once you let go slider.value = strongSelf.disappearingMessagesConfiguration.durationIndex; [slider addTarget:strongSelf action:@selector(durationSliderDidChange:) forControlEvents:UIControlEventValueChanged]; [cell.contentView addSubview:slider]; [slider autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topRow withOffset:6]; [slider autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:rowLabel]; [slider autoPinTrailingToSuperviewMargin]; [slider autoPinBottomToSuperviewMargin]; slider.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"disappearing_messages_slider"); cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"disappearing_messages_duration"); return cell; } customRowHeight:UITableViewAutomaticDimension actionBlock:nil]]; } } #ifdef SHOW_COLOR_PICKER [mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); ConversationColorName colorName = strongSelf.thread.conversationColorName; UIColor *currentColor = [OWSConversationColor conversationColorOrDefaultForColorName:colorName].themeColor; NSString *title = NSLocalizedString(@"CONVERSATION_SETTINGS_CONVERSATION_COLOR", @"Label for table cell which leads to picking a new conversation color"); return [strongSelf cellWithName:title icon:ThemeIconColorPalette disclosureIconColor:currentColor accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"conversation_color")]; } actionBlock:^{ [weakSelf showColorPicker]; }]]; #endif [contents addSection:mainSection]; // Group settings section. if (self.isGroupThread) { NSMutableArray *groupItems = [NSMutableArray new]; if (canEditSharedConversationSettings) { [groupItems addObject:[OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [weakSelf disclosureCellWithName:NSLocalizedString(@"EDIT_GROUP_ACTION", @"table cell label in conversation settings") icon:ThemeIconSettingsEditGroup accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"edit_group")]; return cell; } actionBlock:^{ [weakSelf showUpdateGroupView:UpdateGroupModeDefault]; }]]; } if (isLocalUserInConversation) { [groupItems addObjectsFromArray:@[ [OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [weakSelf disclosureCellWithName:NSLocalizedString(@"LIST_GROUP_MEMBERS_ACTION", @"table cell label in conversation settings") icon:ThemeIconSettingsShowGroup accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"group_members")]; return cell; } actionBlock:^{ [weakSelf showGroupMembersView]; }], [OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [weakSelf disclosureCellWithName:NSLocalizedString(@"LEAVE_GROUP_ACTION", @"table cell label in conversation settings") icon:ThemeIconSettingsLeaveGroup accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"leave_group")]; return cell; } actionBlock:^{ [weakSelf didTapLeaveGroup]; }], ]]; } if (groupItems.count > 0) { [contents addSection:[OWSTableSection sectionWithTitle:NSLocalizedString(@"GROUP_MANAGEMENT_SECTION", @"Conversation settings table section title") items:groupItems]]; } } // Mute thread section. if (!isNoteToSelf) { OWSTableSection *notificationsSection = [OWSTableSection new]; // We need a section header to separate the notifications UI from the group settings UI. notificationsSection.headerTitle = NSLocalizedString( @"SETTINGS_SECTION_NOTIFICATIONS", @"Label for the notifications section of conversation settings view."); [notificationsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; [OWSTableItem configureCell:cell]; OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); cell.preservesSuperviewLayoutMargins = YES; cell.contentView.preservesSuperviewLayoutMargins = YES; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; UIImageView *iconView = [strongSelf viewForIcon:ThemeIconSettingsMessageSound]; UILabel *rowLabel = [UILabel new]; rowLabel.text = NSLocalizedString(@"SETTINGS_ITEM_NOTIFICATION_SOUND", @"Label for settings view that allows user to change the notification sound."); rowLabel.textColor = Theme.primaryTextColor; rowLabel.font = OWSTableItem.primaryLabelFont; rowLabel.lineBreakMode = NSLineBreakByTruncatingTail; UIStackView *contentRow = [[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel ]]; contentRow.spacing = strongSelf.iconSpacing; contentRow.alignment = UIStackViewAlignmentCenter; [cell.contentView addSubview:contentRow]; [contentRow autoPinEdgesToSuperviewMargins]; OWSSound sound = [OWSSounds notificationSoundForThread:strongSelf.thread]; cell.detailTextLabel.text = [OWSSounds displayNameForSound:sound]; cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"notifications"); return cell; } customRowHeight:UITableViewAutomaticDimension actionBlock:^{ OWSSoundSettingsViewController *vc = [OWSSoundSettingsViewController new]; vc.thread = weakSelf.thread; [weakSelf.navigationController pushViewController:vc animated:YES]; }]]; [notificationsSection addItem: [OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil]; [OWSTableItem configureCell:cell]; OWSConversationSettingsViewController *strongSelf = weakSelf; OWSCAssertDebug(strongSelf); cell.preservesSuperviewLayoutMargins = YES; cell.contentView.preservesSuperviewLayoutMargins = YES; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; UIImageView *iconView = [strongSelf viewForIcon:ThemeIconSettingsMuted]; UILabel *rowLabel = [UILabel new]; rowLabel.text = NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_LABEL", @"label for 'mute thread' cell in conversation settings"); rowLabel.textColor = Theme.primaryTextColor; rowLabel.font = OWSTableItem.primaryLabelFont; rowLabel.lineBreakMode = NSLineBreakByTruncatingTail; NSString *muteStatus = NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_NOT_MUTED", @"Indicates that the current thread is not muted."); NSDate *mutedUntilDate = strongSelf.thread.mutedUntilDate; NSDate *now = [NSDate date]; if (mutedUntilDate != nil && [mutedUntilDate timeIntervalSinceDate:now] > 0) { NSCalendar *calendar = [NSCalendar currentCalendar]; NSCalendarUnit calendarUnits = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay; NSDateComponents *muteUntilComponents = [calendar components:calendarUnits fromDate:mutedUntilDate]; NSDateComponents *nowComponents = [calendar components:calendarUnits fromDate:now]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; if (nowComponents.year != muteUntilComponents.year || nowComponents.month != muteUntilComponents.month || nowComponents.day != muteUntilComponents.day) { [dateFormatter setDateStyle:NSDateFormatterShortStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; } else { [dateFormatter setDateStyle:NSDateFormatterNoStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; } muteStatus = [NSString stringWithFormat:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTED_UNTIL_FORMAT", @"Indicates that this thread is muted until a given date or time. " @"Embeds {{The date or time which the thread is muted until}}."), [dateFormatter stringFromDate:mutedUntilDate]]; } UIStackView *contentRow = [[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel ]]; contentRow.spacing = strongSelf.iconSpacing; contentRow.alignment = UIStackViewAlignmentCenter; [cell.contentView addSubview:contentRow]; [contentRow autoPinEdgesToSuperviewMargins]; cell.detailTextLabel.text = muteStatus; cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(OWSConversationSettingsViewController, @"mute"); return cell; } customRowHeight:UITableViewAutomaticDimension actionBlock:^{ [weakSelf showMuteUnmuteActionSheet]; }]]; notificationsSection.footerTitle = NSLocalizedString( @"MUTE_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of muting a thread."); [contents addSection:notificationsSection]; } // Block Conversation section. if (!isNoteToSelf) { OWSTableSection *section = [OWSTableSection new]; if (self.thread.isGroupThread) { section.footerTitle = NSLocalizedString( @"BLOCK_GROUP_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of blocking a group."); } else { section.footerTitle = NSLocalizedString( @"BLOCK_USER_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of blocking another user."); } [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ OWSConversationSettingsViewController *strongSelf = weakSelf; if (!strongSelf) { return [UITableViewCell new]; } NSString *cellTitle; if (strongSelf.thread.isGroupThread) { cellTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_BLOCK_THIS_GROUP", @"table cell label in conversation settings"); } else { cellTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_BLOCK_THIS_USER", @"table cell label in conversation settings"); } UITableViewCell *cell = [strongSelf disclosureCellWithName:cellTitle icon:ThemeIconSettingsBlock accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"block")]; cell.selectionStyle = UITableViewCellSelectionStyleNone; UISwitch *switchView = [UISwitch new]; switchView.on = [strongSelf.blockingManager isThreadBlocked:strongSelf.thread]; [switchView addTarget:strongSelf action:@selector(blockConversationSwitchDidChange:) forControlEvents:UIControlEventValueChanged]; cell.accessoryView = switchView; switchView.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME( OWSConversationSettingsViewController, @"block_conversation_switch"); return cell; } actionBlock:nil]]; [contents addSection:section]; } self.contents = contents; } - (CGFloat)iconSpacing { return 12.f; } - (UITableViewCell *)cellWithName:(NSString *)name icon:(ThemeIcon)icon disclosureIconColor:(UIColor *)disclosureIconColor { UITableViewCell *cell = [self cellWithName:name icon:icon]; OWSColorPickerAccessoryView *accessoryView = [[OWSColorPickerAccessoryView alloc] initWithColor:disclosureIconColor]; [accessoryView sizeToFit]; cell.accessoryView = accessoryView; return cell; } - (UITableViewCell *)cellWithName:(NSString *)name icon:(ThemeIcon)icon { UIImageView *iconView = [self viewForIcon:icon]; return [self cellWithName:name iconView:iconView]; } - (UITableViewCell *)cellWithName:(NSString *)name iconView:(UIView *)iconView { OWSAssertDebug(name.length > 0); UITableViewCell *cell = [OWSTableItem newCell]; cell.preservesSuperviewLayoutMargins = YES; cell.contentView.preservesSuperviewLayoutMargins = YES; UILabel *rowLabel = [UILabel new]; rowLabel.text = name; rowLabel.textColor = Theme.primaryTextColor; rowLabel.font = OWSTableItem.primaryLabelFont; rowLabel.lineBreakMode = NSLineBreakByTruncatingTail; UIStackView *contentRow = [[UIStackView alloc] initWithArrangedSubviews:@[ iconView, rowLabel ]]; contentRow.spacing = self.iconSpacing; [cell.contentView addSubview:contentRow]; [contentRow autoPinEdgesToSuperviewMargins]; return cell; } - (UITableViewCell *)disclosureCellWithName:(NSString *)name icon:(ThemeIcon)icon accessibilityIdentifier:(NSString *)accessibilityIdentifier { UITableViewCell *cell = [self cellWithName:name icon:icon]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.accessibilityIdentifier = accessibilityIdentifier; return cell; } - (UITableViewCell *)labelCellWithName:(NSString *)name icon:(ThemeIcon)icon accessibilityIdentifier:(NSString *)accessibilityIdentifier { UITableViewCell *cell = [self cellWithName:name icon:icon]; cell.accessoryType = UITableViewCellAccessoryNone; cell.accessibilityIdentifier = accessibilityIdentifier; return cell; } - (UIView *)mainSectionHeader { UIView *mainSectionHeader = [UIView new]; mainSectionHeader.backgroundColor = Theme.tableViewBackgroundColor; UIView *threadInfoView = [UIView containerView]; [mainSectionHeader addSubview:threadInfoView]; [threadInfoView autoPinWidthToSuperviewWithMargin:16.f]; [threadInfoView autoPinHeightToSuperviewWithMargin:16.f]; UIImage *avatarImage = [OWSAvatarBuilder buildImageForThread:self.thread diameter:kMediumAvatarSize]; OWSAssertDebug(avatarImage); AvatarImageView *avatarView = [[AvatarImageView alloc] initWithImage:avatarImage]; _avatarView = avatarView; [threadInfoView addSubview:avatarView]; [avatarView autoVCenterInSuperview]; [avatarView autoPinLeadingToSuperviewMargin]; [avatarView autoSetDimension:ALDimensionWidth toSize:kMediumAvatarSize]; [avatarView autoSetDimension:ALDimensionHeight toSize:kMediumAvatarSize]; if (self.isGroupThread && !self.hasSavedGroupIcon && self.canEditSharedConversationSettings) { UIImageView *cameraImageView = [UIImageView new]; [cameraImageView setTemplateImageName:@"camera-outline-24" tintColor:Theme.secondaryTextAndIconColor]; [threadInfoView addSubview:cameraImageView]; [cameraImageView autoSetDimensionsToSize:CGSizeMake(32, 32)]; cameraImageView.contentMode = UIViewContentModeCenter; cameraImageView.backgroundColor = Theme.backgroundColor; cameraImageView.layer.cornerRadius = 16; cameraImageView.layer.shadowColor = [(Theme.isDarkThemeEnabled ? Theme.darkThemeWashColor : Theme.primaryTextColor) CGColor]; cameraImageView.layer.shadowOffset = CGSizeMake(1, 1); cameraImageView.layer.shadowOpacity = 0.5; cameraImageView.layer.shadowRadius = 4; [cameraImageView autoPinTrailingToEdgeOfView:avatarView]; [cameraImageView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:avatarView]; } UIView *threadNameView = [UIView containerView]; [threadInfoView addSubview:threadNameView]; [threadNameView autoVCenterInSuperview]; [threadNameView autoPinTrailingToSuperviewMargin]; [threadNameView autoPinLeadingToTrailingEdgeOfView:avatarView offset:16.f]; UILabel *threadTitleLabel = [UILabel new]; threadTitleLabel.text = self.threadName; threadTitleLabel.textColor = Theme.primaryTextColor; threadTitleLabel.font = [UIFont ows_dynamicTypeTitle2Font]; threadTitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; [threadNameView addSubview:threadTitleLabel]; [threadTitleLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; [threadTitleLabel autoPinWidthToSuperview]; __block UIView *lastTitleView = threadTitleLabel; const CGFloat kSubtitlePointSize = 12.f; void (^addSubtitle)(NSAttributedString *) = ^(NSAttributedString *subtitle) { UILabel *subtitleLabel = [UILabel new]; subtitleLabel.textColor = Theme.secondaryTextAndIconColor; subtitleLabel.font = [UIFont ows_regularFontWithSize:kSubtitlePointSize]; subtitleLabel.attributedText = subtitle; subtitleLabel.lineBreakMode = NSLineBreakByTruncatingTail; [threadNameView addSubview:subtitleLabel]; [subtitleLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastTitleView]; [subtitleLabel autoPinLeadingToSuperviewMargin]; lastTitleView = subtitleLabel; }; if ([self.thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)self.thread; NSString *threadName = [self.contactsManager displayNameForThreadWithSneakyTransaction:contactThread]; SignalServiceAddress *recipientAddress = contactThread.contactAddress; NSString *_Nullable phoneNumber = recipientAddress.phoneNumber; if (phoneNumber.length > 0) { NSString *formattedPhoneNumber = [PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:phoneNumber]; if (![threadName isEqualToString:formattedPhoneNumber]) { NSAttributedString *subtitle = [[NSAttributedString alloc] initWithString:formattedPhoneNumber]; addSubtitle(subtitle); } } __block NSString *_Nullable username; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *transaction) { username = [self.profileManager usernameForAddress:recipientAddress transaction:transaction]; }]; if (username.length > 0) { NSString *formattedUsername = [CommonFormats formatUsername:username]; if (![threadName isEqualToString:formattedUsername]) { addSubtitle([[NSAttributedString alloc] initWithString:formattedUsername]); } } if (!RemoteConfig.messageRequests && ![self.contactsManager hasNameInSystemContactsForAddress:recipientAddress]) { NSString *_Nullable profileName = [self.contactsManager formattedProfileNameForAddress:recipientAddress]; if (profileName) { addSubtitle([[NSAttributedString alloc] initWithString:profileName]); } } #if DEBUG NSString *uuidText = [NSString stringWithFormat:@"UUID: %@", contactThread.contactAddress.uuid ?: @"Unknown"]; addSubtitle([[NSAttributedString alloc] initWithString:uuidText]); #endif BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForAddress:recipientAddress] == OWSVerificationStateVerified; if (isVerified) { NSMutableAttributedString *subtitle = [NSMutableAttributedString new]; // "checkmark" [subtitle appendAttributedString:[[NSAttributedString alloc] initWithString:LocalizationNotNeeded(@"\uf00c ") attributes:@{ NSFontAttributeName : [UIFont ows_fontAwesomeFont:kSubtitlePointSize], }]]; [subtitle appendAttributedString:[[NSAttributedString alloc] initWithString:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_BADGE", @"Badge indicating that the user is verified.")]]; addSubtitle(subtitle); } } // TODO Message Request: In order to debug the profile is getting shared in the right moments, // display the thread whitelist state in settings. Eventually we can probably delete this. #if DEBUG __block BOOL isThreadInProfileWhitelist; [self.databaseStorage uiReadWithBlock:^(SDSAnyReadTransaction *transaction) { isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread transaction:transaction]; }]; NSString *hasSharedProfile = [NSString stringWithFormat:@"Whitelisted: %@", isThreadInProfileWhitelist ? @"Yes" : @"No"]; addSubtitle([[NSAttributedString alloc] initWithString:hasSharedProfile]); #endif [lastTitleView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; if (self.canEditSharedConversationSettings) { [mainSectionHeader addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(conversationNameTouched:)]]; } mainSectionHeader.userInteractionEnabled = YES; SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, mainSectionHeader); return mainSectionHeader; } - (void)conversationNameTouched:(UIGestureRecognizer *)sender { if (!self.canEditSharedConversationSettings) { OWSFailDebug(@"failure: !self.canEditSharedConversationSettings"); return; } if (sender.state == UIGestureRecognizerStateRecognized) { if (self.isGroupThread) { CGPoint location = [sender locationInView:self.avatarView]; if (CGRectContainsPoint(self.avatarView.bounds, location)) { [self showUpdateGroupView:UpdateGroupModeEditGroupAvatar]; } else { [self showUpdateGroupView:UpdateGroupModeEditGroupName]; } } else { if (self.contactsManager.supportsContactEditing) { [self presentContactViewController]; } } } } - (UIImageView *)viewForIcon:(ThemeIcon)icon { UIImage *iconImage = [Theme iconImage:icon]; OWSAssertDebug(iconImage); UIImageView *iconView = [[UIImageView alloc] initWithImage:iconImage]; iconView.tintColor = Theme.primaryIconColor; iconView.contentMode = UIViewContentModeScaleAspectFit; iconView.layer.minificationFilter = kCAFilterTrilinear; iconView.layer.magnificationFilter = kCAFilterTrilinear; [iconView autoSetDimensionsToSize:CGSizeMake(kIconViewLength, kIconViewLength)]; return iconView; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSIndexPath *_Nullable selectedPath = [self.tableView indexPathForSelectedRow]; if (selectedPath) { // HACK to unselect rows when swiping back // http://stackoverflow.com/questions/19379510/uitableviewcell-doesnt-get-deselected-when-swiping-back-quickly [self.tableView deselectRowAtIndexPath:selectedPath animated:animated]; } [self updateTableContents]; } - (BOOL)hasUnsavedChangesToDisappearingMessagesConfiguration { __block BOOL hasChanged; [self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) { hasChanged = [self.disappearingMessagesConfiguration hasChangedWithTransaction:transaction]; }]; return hasChanged; } #pragma mark - OWSNavigationView - (BOOL)shouldCancelNavigationBack { BOOL result = self.hasUnsavedChangesToDisappearingMessagesConfiguration; if (result) { [self updateDisappearingMessagesConfigurationAndDismiss]; } return result; } - (void)updateDisappearingMessagesConfigurationAndDismiss { [self updateDisappearingMessagesConfigurationObjc:self.disappearingMessagesConfiguration thread:self.thread]; } #pragma mark - Actions - (void)showShareProfileAlert { [self.profileManager presentAddThreadToProfileWhitelist:self.thread fromViewController:self success:^{ [self updateTableContents]; }]; } - (void)showVerificationView { OWSAssertDebug([self.thread isKindOfClass:[TSContactThread class]]); TSContactThread *contactThread = (TSContactThread *)self.thread; SignalServiceAddress *contactAddress = contactThread.contactAddress; OWSAssertDebug(contactAddress.isValid); [FingerprintViewController presentFromViewController:self address:contactAddress]; } - (void)showGroupMembersView { ShowGroupMembersViewController *showGroupMembersViewController = [ShowGroupMembersViewController new]; [showGroupMembersViewController configWithThread:(TSGroupThread *)self.thread]; [self.navigationController pushViewController:showGroupMembersViewController animated:YES]; } - (void)showUpdateGroupView:(UpdateGroupMode)mode { if (!self.canEditSharedConversationSettings) { OWSFailDebug(@"failure: !self.canEditSharedConversationSettings"); return; } OWSAssertDebug(self.conversationSettingsViewDelegate); TSGroupThread *groupThread = (TSGroupThread *)self.thread; UpdateGroupViewController *updateGroupViewController = [[UpdateGroupViewController alloc] initWithGroupThread:groupThread mode:mode]; updateGroupViewController.conversationSettingsViewDelegate = self.conversationSettingsViewDelegate; [self.navigationController pushViewController:updateGroupViewController animated:YES]; } - (void)presentContactViewController { if (!self.contactsManager.supportsContactEditing) { OWSFailDebug(@"Contact editing not supported"); return; } if (![self.thread isKindOfClass:[TSContactThread class]]) { OWSFailDebug(@"unexpected thread: %@", [self.thread class]); return; } TSContactThread *contactThread = (TSContactThread *)self.thread; CNContactViewController *_Nullable contactViewController = [self.contactsViewHelper contactViewControllerForAddress:contactThread.contactAddress editImmediately:YES]; if (!contactViewController) { OWSFailDebug(@"Unexpectedly missing contact VC"); return; } contactViewController.delegate = self; [self.navigationController pushViewController:contactViewController animated:YES]; } - (void)presentAddToContactViewControllerWithAddress:(SignalServiceAddress *)address { if (!self.contactsManager.supportsContactEditing) { // Should not expose UI that lets the user get here. OWSFailDebug(@"Contact editing not supported."); return; } if (!self.contactsManager.isSystemContactsAuthorized) { [self.contactsViewHelper presentMissingContactAccessAlertControllerFromViewController:self]; return; } OWSAddToContactViewController *viewController = [OWSAddToContactViewController new]; [viewController configureWithAddress:address]; [self.navigationController pushViewController:viewController animated:YES]; } - (void)didTapLeaveGroup { ActionSheetController *alert = [[ActionSheetController alloc] initWithTitle:NSLocalizedString(@"CONFIRM_LEAVE_GROUP_TITLE", @"Alert title") message:NSLocalizedString(@"CONFIRM_LEAVE_GROUP_DESCRIPTION", @"Alert body")]; ActionSheetAction *leaveAction = [[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"LEAVE_BUTTON_TITLE", @"Confirmation button within contextual alert") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"leave_group_confirm") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull action) { [self leaveGroup]; }]; [alert addAction:leaveAction]; [alert addAction:[OWSActionSheets cancelAction]]; [self presentActionSheet:alert]; } - (void)leaveGroup { TSGroupThread *groupThread = (TSGroupThread *)self.thread; [GroupManager leaveGroupOrDeclineInviteAsyncWithUIWithGroupThread:groupThread fromViewController:self replacementAdminUuid:nil success:^{ [self.navigationController popViewControllerAnimated:YES]; }]; } - (void)disappearingMessagesSwitchValueDidChange:(UISwitch *)sender { OWSAssertDebug(self.canEditSharedConversationSettings); UISwitch *disappearingMessagesSwitch = (UISwitch *)sender; [self toggleDisappearingMessages:disappearingMessagesSwitch.isOn]; [self updateTableContents]; } - (void)blockConversationSwitchDidChange:(id)sender { if (![sender isKindOfClass:[UISwitch class]]) { OWSFailDebug(@"Unexpected sender for block user switch: %@", sender); } UISwitch *blockConversationSwitch = (UISwitch *)sender; BOOL isCurrentlyBlocked = [self.blockingManager isThreadBlocked:self.thread]; __weak OWSConversationSettingsViewController *weakSelf = self; if (blockConversationSwitch.isOn) { OWSAssertDebug(!isCurrentlyBlocked); if (isCurrentlyBlocked) { return; } [BlockListUIUtils showBlockThreadActionSheet:self.thread fromViewController:self completionBlock:^(BOOL isBlocked) { // Update switch state if user cancels action. blockConversationSwitch.on = isBlocked; [weakSelf updateTableContents]; }]; } else { OWSAssertDebug(isCurrentlyBlocked); if (!isCurrentlyBlocked) { return; } [BlockListUIUtils showUnblockThreadActionSheet:self.thread fromViewController:self completionBlock:^(BOOL isBlocked) { // Update switch state if user cancels action. blockConversationSwitch.on = isBlocked; [weakSelf updateTableContents]; }]; } } - (void)toggleDisappearingMessages:(BOOL)flag { OWSAssertDebug(self.canEditSharedConversationSettings); self.disappearingMessagesConfiguration = [self.disappearingMessagesConfiguration copyWithIsEnabled:flag]; [self updateTableContents]; } - (void)durationSliderDidChange:(UISlider *)slider { OWSAssertDebug(self.canEditSharedConversationSettings); NSUInteger index = (NSUInteger)(slider.value + 0.5); if (!slider.isTracking) { // Snap the slider to a valid value unless the user // is still interacting with the control. [slider setValue:index animated:YES]; } NSNumber *numberOfSeconds = self.disappearingMessagesDurations[index]; uint32_t durationSeconds = [numberOfSeconds unsignedIntValue]; self.disappearingMessagesConfiguration = [self.disappearingMessagesConfiguration copyAsEnabledWithDurationSeconds:durationSeconds]; [self updateDisappearingMessagesDurationLabel]; } - (void)updateDisappearingMessagesDurationLabel { if (self.disappearingMessagesConfiguration.isEnabled) { NSString *keepForFormat = NSLocalizedString(@"KEEP_MESSAGES_DURATION", @"Slider label embeds {{TIME_AMOUNT}}, e.g. '2 hours'. See *_TIME_AMOUNT strings for examples."); self.disappearingMessagesDurationLabel.text = [NSString stringWithFormat:keepForFormat, self.disappearingMessagesConfiguration.durationString]; } else { self.disappearingMessagesDurationLabel.text = NSLocalizedString(@"KEEP_MESSAGES_FOREVER", @"Slider label when disappearing messages is off"); } [self.disappearingMessagesDurationLabel setNeedsLayout]; [self.disappearingMessagesDurationLabel.superview setNeedsLayout]; } - (void)showMuteUnmuteActionSheet { // The "unmute" action sheet has no title or message; the // action label speaks for itself. NSString *title = nil; NSString *message = nil; if (!self.thread.isMuted) { title = NSLocalizedString( @"CONVERSATION_SETTINGS_MUTE_ACTION_SHEET_TITLE", @"Title of the 'mute this thread' action sheet."); message = NSLocalizedString( @"MUTE_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of muting a thread."); } ActionSheetController *actionSheet = [[ActionSheetController alloc] initWithTitle:title message:message]; __weak OWSConversationSettingsViewController *weakSelf = self; if (self.thread.isMuted) { ActionSheetAction *action = [[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_UNMUTE_ACTION", @"Label for button to unmute a thread.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"unmute") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { [weakSelf setThreadMutedUntilDate:nil]; }]; [actionSheet addAction:action]; } else { #ifdef DEBUG [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_ONE_MINUTE_ACTION", @"Label for button to mute a thread for a minute.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"mute_1_minute") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; NSCalendar *calendar = [NSCalendar currentCalendar]; [calendar setTimeZone:timeZone]; NSDateComponents *dateComponents = [NSDateComponents new]; [dateComponents setMinute:1]; NSDate *mutedUntilDate = [calendar dateByAddingComponents:dateComponents toDate:[NSDate date] options:0]; [weakSelf setThreadMutedUntilDate:mutedUntilDate]; }]]; #endif [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_ONE_HOUR_ACTION", @"Label for button to mute a thread for a hour.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"mute_1_hour") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; NSCalendar *calendar = [NSCalendar currentCalendar]; [calendar setTimeZone:timeZone]; NSDateComponents *dateComponents = [NSDateComponents new]; [dateComponents setHour:1]; NSDate *mutedUntilDate = [calendar dateByAddingComponents:dateComponents toDate:[NSDate date] options:0]; [weakSelf setThreadMutedUntilDate:mutedUntilDate]; }]]; [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_ONE_DAY_ACTION", @"Label for button to mute a thread for a day.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"mute_1_day") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; NSCalendar *calendar = [NSCalendar currentCalendar]; [calendar setTimeZone:timeZone]; NSDateComponents *dateComponents = [NSDateComponents new]; [dateComponents setDay:1]; NSDate *mutedUntilDate = [calendar dateByAddingComponents:dateComponents toDate:[NSDate date] options:0]; [weakSelf setThreadMutedUntilDate:mutedUntilDate]; }]]; [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_ONE_WEEK_ACTION", @"Label for button to mute a thread for a week.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"mute_1_week") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; NSCalendar *calendar = [NSCalendar currentCalendar]; [calendar setTimeZone:timeZone]; NSDateComponents *dateComponents = [NSDateComponents new]; [dateComponents setDay:7]; NSDate *mutedUntilDate = [calendar dateByAddingComponents:dateComponents toDate:[NSDate date] options:0]; [weakSelf setThreadMutedUntilDate:mutedUntilDate]; }]]; [actionSheet addAction:[[ActionSheetAction alloc] initWithTitle:NSLocalizedString(@"CONVERSATION_SETTINGS_MUTE_ONE_YEAR_ACTION", @"Label for button to mute a thread for a year.") accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"mute_1_year") style:ActionSheetActionStyleDestructive handler:^(ActionSheetAction *_Nonnull ignore) { NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; NSCalendar *calendar = [NSCalendar currentCalendar]; [calendar setTimeZone:timeZone]; NSDateComponents *dateComponents = [NSDateComponents new]; [dateComponents setYear:1]; NSDate *mutedUntilDate = [calendar dateByAddingComponents:dateComponents toDate:[NSDate date] options:0]; [weakSelf setThreadMutedUntilDate:mutedUntilDate]; }]]; } [actionSheet addAction:[OWSActionSheets cancelAction]]; [self presentActionSheet:actionSheet]; } - (void)setThreadMutedUntilDate:(nullable NSDate *)value { [self.databaseStorage writeWithBlock:^(SDSAnyWriteTransaction *transaction) { [self.thread updateWithMutedUntilDate:value transaction:transaction]; }]; [self updateTableContents]; } - (void)showMediaGallery { OWSLogDebug(@""); MediaTileViewController *tileVC = [[MediaTileViewController alloc] initWithThread:self.thread]; [self.navigationController pushViewController:tileVC animated:YES]; } - (void)tappedConversationSearch { [self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch]; } #pragma mark - Notifications - (void)identityStateDidChange:(NSNotification *)notification { OWSAssertIsOnMainThread(); [self updateTableContents]; } - (void)otherUsersProfileDidChange:(NSNotification *)notification { OWSAssertIsOnMainThread(); SignalServiceAddress *address = notification.userInfo[kNSNotificationKey_ProfileAddress]; OWSAssertDebug(address.isValid); TSContactThread *_Nullable contactThread; if ([self.thread isKindOfClass:[TSContactThread class]]) { contactThread = (TSContactThread *)self.thread; } if (address.isValid && contactThread && [contactThread.contactAddress isEqualToAddress:address]) { [self updateTableContents]; } } - (void)profileWhitelistDidChange:(NSNotification *)notification { OWSAssertIsOnMainThread(); // If profile whitelist just changed, we may need to refresh the view. SignalServiceAddress *_Nullable address = notification.userInfo[kNSNotificationKey_ProfileAddress]; NSData *_Nullable groupId = notification.userInfo[kNSNotificationKey_ProfileGroupId]; if (address != nil && !self.thread.isGroupThread) { TSContactThread *contactThread = (TSContactThread *)self.thread; if ([contactThread.contactAddress isEqualToAddress:address]) { [self updateTableContents]; } } if (groupId.length > 0 && self.thread.isGroupThread) { TSGroupThread *groupThread = (TSGroupThread *)self.thread; if ([groupThread.groupModel.groupId isEqualToData:groupId]) { [self updateTableContents]; } } } #pragma mark - ColorPickerDelegate #ifdef SHOW_COLOR_PICKER - (void)showColorPicker { OWSSheetViewController *sheetViewController = self.colorPicker.sheetViewController; sheetViewController.delegate = self; [self presentViewController:sheetViewController animated:YES completion:^() { OWSLogInfo(@"presented sheet view"); }]; } - (void)colorPicker:(OWSColorPicker *)colorPicker didPickConversationColor:(OWSConversationColor *_Nonnull)conversationColor { OWSLogDebug(@"picked color: %@", conversationColor.name); [self.databaseStorage writeWithBlock:^(SDSAnyWriteTransaction *transaction) { [self.thread updateConversationColorName:conversationColor.name transaction:transaction]; }]; [self.contactsManager.avatarCache removeAllImages]; [self.contactsManager clearColorNameCache]; [self updateTableContents]; [self.conversationSettingsViewDelegate conversationColorWasUpdated]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ConversationConfigurationSyncOperation *operation = [[ConversationConfigurationSyncOperation alloc] initWithThread:self.thread]; OWSAssertDebug(operation.isReady); [operation start]; }); } #endif #pragma mark - OWSSheetViewController - (void)sheetViewControllerRequestedDismiss:(OWSSheetViewController *)sheetViewController { [self dismissViewControllerAnimated:YES completion:nil]; } @end NS_ASSUME_NONNULL_END