diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 994a31ff91..dddd467bdc 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -630,6 +630,7 @@ 8821334E2304E72700353626 /* NonContactTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8821334D2304E72700353626 /* NonContactTableViewCell.swift */; }; 8827004C232071C500F01C46 /* OWSWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8827004B232071C500F01C46 /* OWSWindow.swift */; }; 8827004E23208A1900F01C46 /* AppearanceSettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8827004D23208A1900F01C46 /* AppearanceSettingsTableViewController.swift */; }; + 882BDAAE249050F000C14587 /* AddToGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882BDAAD249050F000C14587 /* AddToGroupViewController.swift */; }; 882E0B7723C7BD1900E8CE49 /* NotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 885AC56423AB42430077705E /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8835DDF9230CEDC300DC6B66 /* RecipientPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8835DDF8230CEDC300DC6B66 /* RecipientPickerViewController.m */; }; 8835DDFD230CEE8900DC6B66 /* RecipientPickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8835DDFC230CEE8900DC6B66 /* RecipientPickerDelegate.swift */; }; @@ -1586,6 +1587,7 @@ 882159D023677E0500D17403 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ur; path = translations/ur.lproj/Localizable.strings; sourceTree = ""; }; 8827004B232071C500F01C46 /* OWSWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSWindow.swift; sourceTree = ""; }; 8827004D23208A1900F01C46 /* AppearanceSettingsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsTableViewController.swift; sourceTree = ""; }; + 882BDAAD249050F000C14587 /* AddToGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToGroupViewController.swift; sourceTree = ""; }; 8835DDF7230CEDC300DC6B66 /* RecipientPickerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RecipientPickerViewController.h; sourceTree = ""; }; 8835DDF8230CEDC300DC6B66 /* RecipientPickerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RecipientPickerViewController.m; sourceTree = ""; }; 8835DDFC230CEE8900DC6B66 /* RecipientPickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientPickerDelegate.swift; sourceTree = ""; }; @@ -2038,6 +2040,7 @@ 347B83F624367EC00019A52C /* GroupViewHelper+MemberActionSheet.swift */, 34E6003924251AD50026AD4B /* GroupViewUtils.swift */, 88A357B823639384009D6B9A /* MemberActionSheet.swift */, + 882BDAAD249050F000C14587 /* AddToGroupViewController.swift */, 340FC898204DAC8D007AEB0F /* OWSAddToContactViewController.h */, 340FC8A1204DAC8D007AEB0F /* OWSAddToContactViewController.m */, 347B83FC24378DDE0019A52C /* PendingGroupMembersViewController.swift */, @@ -4804,6 +4807,7 @@ 34BE2F5822A7371D002C94AC /* PdfViewController.swift in Sources */, 3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, + 882BDAAE249050F000C14587 /* AddToGroupViewController.swift in Sources */, 8806EF19248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift in Sources */, 343A65951FC47D5E000477A1 /* DebugUISyncMessages.m in Sources */, 88D23D1223CEBFB200B0E74B /* UserNotificationActionHandler.swift in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 4c2c994e88..8f0dbee8a0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -643,7 +643,7 @@ NS_ASSUME_NONNULL_BEGIN return NO; } - CGPoint tapPoint = [sender locationInView:self]; + CGPoint tapPoint = [sender locationInView:self.avatarView.superview]; return CGRectContainsPoint(self.avatarView.frame, tapPoint); } @@ -678,7 +678,8 @@ NS_ASSUME_NONNULL_BEGIN CGPoint velocity = [self.panGestureRecognizer velocityInView:self]; return fabs(velocity.x) > fabs(velocity.y); } else if (gestureRecognizer == self.messageViewTapGestureRecognizer) { - return ![self isGestureInReactions:self.messageViewTapGestureRecognizer] && + return ![self isGestureInReactions:self.messageViewTapGestureRecognizer] + && ![self isGestureInAvatar:self.contentViewTapGestureRecognizer] && [self.messageView willHandleTapGesture:self.messageViewTapGestureRecognizer]; } else if (gestureRecognizer == self.contentViewTapGestureRecognizer) { return [self isGestureInAvatar:self.contentViewTapGestureRecognizer] || diff --git a/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift b/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift new file mode 100644 index 0000000000..0927df731e --- /dev/null +++ b/Signal/src/ViewControllers/ThreadSettings/AddToGroupViewController.swift @@ -0,0 +1,293 @@ +// +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +@objc +public class AddToGroupViewController: OWSTableViewController { + + // MARK: Dependencies + + private var databaseStorage: SDSDatabaseStorage { + return SDSDatabaseStorage.shared + } + + // MARK: - + + private let address: SignalServiceAddress + private let collation = UILocalizedIndexedCollation.current() + private let maxRecentGroups = 5 + + private lazy var threadViewHelper: ThreadViewHelper = { + let threadViewHelper = ThreadViewHelper() + threadViewHelper.delegate = self + return threadViewHelper + }() + + init(address: SignalServiceAddress) { + self.address = address + + super.init() + + tableViewStyle = .plain + } + + public class func presentForUser(_ address: SignalServiceAddress, + from fromViewController: UIViewController) { + AssertIsOnMainThread() + + let view = AddToGroupViewController(address: address) + let modal = OWSNavigationController(rootViewController: view) + fromViewController.presentFormSheet(modal, animated: true) + } + + // MARK: - + + public override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = NSLocalizedString("ADD_TO_GROUP_TITLE", comment: "Title of the 'add to group' view.") + + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(didPressCloseButton)) + + tableView.separatorStyle = .none + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + updateTableContents() + + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .ThemeDidChange, object: nil) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + NotificationCenter.default.removeObserver(self) + } + + private func updateTableContents() { + AssertIsOnMainThread() + + let groupThreads = threadViewHelper.threads.filter { thread -> Bool in + guard let groupThread = thread as? TSGroupThread else { return false } + guard !OWSBlockingManager.shared().isThreadBlocked(groupThread) else { return false } + return groupThread.isLocalUserInGroup + } as? [TSGroupThread] ?? [] + + let contents = OWSTableContents() + + let recentGroups = groupThreads.count > maxRecentGroups ? Array(groupThreads[0.. maxRecentGroups ? Array(groupThreads[maxRecentGroups.. TSGroupModel? { + do { + return try databaseStorage.read { transaction in + var builder = oldGroupModel.asBuilder + let oldGroupMembership = oldGroupModel.groupMembership + var groupMembershipBuilder = oldGroupMembership.asBuilder + + guard !oldGroupMembership.isPendingOrNonPendingMember(self.address) else { + owsFailDebug("Recipient is already in group.") + return nil + } + // GroupManager will separate out members as pending if necessary. + groupMembershipBuilder.addNonPendingMember(self.address, role: .normal) + + builder.groupMembership = groupMembershipBuilder.build() + return try builder.build(transaction: transaction) + } + } catch { + owsFailDebug("Error: \(error)") + return nil + } + } + + func updateGroupThreadPromise(oldGroupModel: TSGroupModel, + newGroupModel: TSGroupModel) -> Promise { + + guard let localAddress = TSAccountManager.localAddress else { + return Promise(error: OWSAssertionError("Missing localAddress.")) + } + + return firstly { () -> Promise in + return GroupManager.messageProcessingPromise(for: oldGroupModel, + description: self.logTag) + }.then(on: .global()) { _ in + // dmConfiguration: nil means don't change disappearing messages configuration. + GroupManager.localUpdateExistingGroup(groupModel: newGroupModel, + dmConfiguration: nil, + groupUpdateSourceAddress: localAddress) + }.asVoid() + } + + // MARK: - + + private func sectionHeader(title: String) -> UIView { + let textView = UITextView() + textView.isOpaque = false + textView.isEditable = false + textView.contentInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.isScrollEnabled = false + textView.textColor = Theme.primaryTextColor + textView.font = UIFont.ows_dynamicTypeBody.ows_semibold() + textView.backgroundColor = Theme.washColor + let tableEdgeInsets: CGFloat = UIDevice.current.isPlusSizePhone ? 20 : 16 + textView.textContainerInset = UIEdgeInsets(top: 5, left: tableEdgeInsets, bottom: 5, right: tableEdgeInsets) + textView.text = title + return textView + } + + private func item(for groupThread: TSGroupThread) -> OWSTableItem { + return OWSTableItem( + customCellBlock: { + let cell = GroupTableViewCell() + cell.configure(thread: groupThread) + return cell + }, + customRowHeight: UITableView.automaticDimension, + actionBlock: { [weak self] in + self?.didSelectGroup(groupThread) + } + ) + } +} + +// MARK: - + +extension AddToGroupViewController: ThreadViewHelperDelegate { + public func threadListDidChange() { + updateTableContents() + } +} diff --git a/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift b/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift index 95f7f59b23..14d5e0cc17 100644 --- a/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift +++ b/Signal/src/ViewControllers/ThreadSettings/MemberActionSheet.swift @@ -122,6 +122,16 @@ class MemberActionSheet: NSObject { blockAction.leadingIcon = .settingsBlock actionSheet.addAction(blockAction) + let addToGroupAction = ActionSheetAction( + title: NSLocalizedString("ADD_TO_GROUP", + comment: "Label for button or row which allows users to add to another group."), + accessibilityIdentifier: "MemberActionSheet.addToGroup" + ) { _ in + AddToGroupViewController.presentForUser(self.address, from: fromViewController) + } + addToGroupAction.leadingIcon = .settingsAddToGroup + actionSheet.addAction(addToGroupAction) + let safetyNumberAction = ActionSheetAction( title: NSLocalizedString("VERIFY_PRIVACY", comment: "Label for button or row which allows users to verify the safety number of another user."), diff --git a/Signal/src/views/GroupTableViewCell.swift b/Signal/src/views/GroupTableViewCell.swift index 4a440168d0..f78ee517b9 100644 --- a/Signal/src/views/GroupTableViewCell.swift +++ b/Signal/src/views/GroupTableViewCell.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. // import UIKit @@ -69,9 +69,8 @@ import SignalServiceKit self.nameLabel.text = MessageStrings.newGroupDefaultTitle } - let groupMembers = thread.groupModel.groupMembers - let groupMemberNames = groupMembers.map { contactsManager.displayName(for: $0) }.joined(separator: ", ") - self.subtitleLabel.text = groupMemberNames + let groupMembersCount = thread.groupModel.groupMembers.count + self.subtitleLabel.text = GroupViewUtils.formatGroupMembersLabel(memberCount: groupMembersCount) self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize) diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 85225846da..be53267dc9 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -68,6 +68,30 @@ /* Message shown in conversation view that offers to add an unknown user to your phone's contacts. */ "ADD_TO_CONTACTS_OFFER" = "Would you like to add this user to your contacts?"; +/* Label for button or row which allows users to add to another group. */ +"ADD_TO_GROUP" = "Add to Another Group"; + +/* The button on the 'add to group' confirmation to add the user to the group. */ +"ADD_TO_GROUP_ACTION_PROCEED_BUTTON" = "Add to Group"; + +/* The title on the 'add to group' confirmation action sheet. Embeds {contact name} */ +"ADD_TO_GROUP_ACTION_SHEET_MESSAGE_FORMAT" = "Add %@ to this group?"; + +/* The title on the 'add to group' confirmation action sheet. Embeds {group name} */ +"ADD_TO_GROUP_ACTION_SHEET_TITLE_FORMAT" = "Add to %@"; + +/* A toast on the 'add to group' view indicating the user is already a member. Embeds {contact name} and {group name} */ +"ADD_TO_GROUP_ALREADY_MEMBER_TOAST_FORMAT" = "%@ is already a member of %@"; + +/* The title for the 'add to group' view's recents section */ +"ADD_TO_GROUP_RECENTS_TITLE" = "Recent"; + +/* A toast on the 'add to group' view indicating the user was added. Embeds {contact name} and {group name} */ +"ADD_TO_GROUP_SUCCESS_TOAST_FORMAT" = "%@ was added to %@"; + +/* Title of the 'add to group' view. */ +"ADD_TO_GROUP_TITLE" = "Add to Another Group"; + /* Message shown in conversation view that offers to share your profile with a user. */ "ADD_USER_TO_PROFILE_WHITELIST_OFFER" = "Would you like to share your profile with this user?"; diff --git a/SignalMessaging/ViewControllers/FindByPhoneNumberViewController.swift b/SignalMessaging/ViewControllers/FindByPhoneNumberViewController.swift index b8ae65f793..d37990dcb6 100644 --- a/SignalMessaging/ViewControllers/FindByPhoneNumberViewController.swift +++ b/SignalMessaging/ViewControllers/FindByPhoneNumberViewController.swift @@ -132,11 +132,17 @@ public class FindByPhoneNumberViewController: OWSViewController { button.setEnabled(false) button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "button") + + applyTheme() } public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) + applyTheme() + } + + private func applyTheme() { view.backgroundColor = Theme.backgroundColor countryRowTitleLabel.textColor = Theme.primaryTextColor phoneNumberRowTitleLabel.textColor = Theme.primaryTextColor diff --git a/SignalMessaging/appearance/Theme+OWS.swift b/SignalMessaging/appearance/Theme+OWS.swift index 2e6ff8f7bc..126908bfbd 100644 --- a/SignalMessaging/appearance/Theme+OWS.swift +++ b/SignalMessaging/appearance/Theme+OWS.swift @@ -28,6 +28,7 @@ public enum ThemeIcon: UInt { case settingsViewRevokeGroupAdmin case settingsViewRemoveFromGroup case settingsViewPendingInvites + case settingsAddToGroup case stickerButton case cameraButton @@ -151,6 +152,8 @@ public extension Theme { return "plus-256" case .settingsShowAllMembers: return "chevron-down-256" + case .settingsAddToGroup: + return isDarkThemeEnabled ? "group-solid-24" : "group-outline-24" // Input Toolbar case .stickerButton: