diff --git a/Signal/Images.xcassets/Loki V2/Circle.imageset/Circle.pdf b/Signal/Images.xcassets/Loki V2/Circle.imageset/Circle.pdf new file mode 100644 index 000000000..254f9a559 Binary files /dev/null and b/Signal/Images.xcassets/Loki V2/Circle.imageset/Circle.pdf differ diff --git a/Signal/Images.xcassets/Loki V2/Circle.imageset/Contents.json b/Signal/Images.xcassets/Loki V2/Circle.imageset/Contents.json new file mode 100644 index 000000000..3d338af2b --- /dev/null +++ b/Signal/Images.xcassets/Loki V2/Circle.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Circle.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index cb4b80d83..daba681ed 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -7,7 +7,7 @@ CarthageVersion 0.34.0 OSXVersion - 10.15.1 + 10.15.2 WebRTCCommit 1445d719bf05280270e9f77576f80f973fd847f8 M73 diff --git a/Signal/src/Loki/Components/ConversationCell.swift b/Signal/src/Loki/Components/ConversationCell.swift index a7028ccc3..2f2d40de1 100644 --- a/Signal/src/Loki/Components/ConversationCell.swift +++ b/Signal/src/Loki/Components/ConversationCell.swift @@ -180,8 +180,8 @@ final class ConversationCell : UITableViewCell { private func getDisplayName() -> String { if threadViewModel.isGroupThread { - if threadViewModel.name.isEmpty { - return NSLocalizedString("New Group", comment: "") + if threadViewModel.name.isEmpty || threadViewModel.name == "New Group" { + return DisplayNameUtilities.getDisplayName(for: threadViewModel.threadRecord as! TSGroupThread) } else { return threadViewModel.name } diff --git a/Signal/src/Loki/Components/ConversationTitleView.swift b/Signal/src/Loki/Components/ConversationTitleView.swift index ed0877458..c87deedcd 100644 --- a/Signal/src/Loki/Components/ConversationTitleView.swift +++ b/Signal/src/Loki/Components/ConversationTitleView.swift @@ -72,8 +72,8 @@ final class ConversationTitleView : UIView { private func updateTitle() { let title: String if thread.isGroupThread() { - if thread.name().isEmpty { - title = NSLocalizedString("New Group", comment: "") + if thread.name().isEmpty || thread.name() == "New Group" { + title = DisplayNameUtilities.getDisplayName(for: thread as! TSGroupThread) } else { title = thread.name() } diff --git a/Signal/src/Loki/View Controllers/NewClosedGroupVC.swift b/Signal/src/Loki/View Controllers/NewClosedGroupVC.swift index 4aa2f2b16..d2b765698 100644 --- a/Signal/src/Loki/View Controllers/NewClosedGroupVC.swift +++ b/Signal/src/Loki/View Controllers/NewClosedGroupVC.swift @@ -1,5 +1,6 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableViewDelegate { + private var selectedContacts: Set = [] private lazy var contacts: [String] = { var result: [String] = [] @@ -41,6 +42,19 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableV navigationBar.shadowImage = UIImage() navigationBar.isTranslucent = false navigationBar.barTintColor = Colors.navigationBarBackground + // Set up navigation bar buttons + let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) + closeButton.tintColor = Colors.text + navigationItem.leftBarButtonItem = closeButton + let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(createClosedGroup)) + doneButton.tintColor = Colors.text + navigationItem.rightBarButtonItem = doneButton + // Customize title + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("New Closed Group", comment: "") + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) + navigationItem.titleView = titleLabel // Set up table view view.addSubview(tableView) tableView.pin(to: view) @@ -55,12 +69,54 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableV let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! Cell let contact = contacts[indexPath.row] cell.hexEncodedPublicKey = contact + cell.hasTick = selectedContacts.contains(contact) return cell } // MARK: Interaction func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // TODO: Implement + let contact = contacts[indexPath.row] + if !selectedContacts.contains(contact) { + selectedContacts.insert(contact) + } else { + selectedContacts.remove(contact) + } + guard let cell = tableView.cellForRow(at: indexPath) as? Cell else { return } + cell.hasTick = selectedContacts.contains(contact) + tableView.deselectRow(at: indexPath, animated: true) + } + + @objc private func close() { + dismiss(animated: true, completion: nil) + } + + @objc private func createClosedGroup() { + let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + let members = [String](selectedContacts) + [ userHexEncodedPublicKey ] + let admins = [ userHexEncodedPublicKey ] + let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(Randomness.generateRandomBytes(kGroupIdLength)!.toHexString()) + let group = TSGroupModel(title: nil, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins) + let thread = TSGroupThread.getOrCreateThread(with: group) + OWSProfileManager.shared().addThread(toProfileWhitelist: thread) + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in + let message = TSOutgoingMessage(in: thread, groupMetaMessage: .new, expiresInSeconds: 0) + message.update(withCustomMessage: NSLocalizedString("GROUP_CREATED", comment: "")) + DispatchQueue.main.async { + SSKEnvironment.shared.messageSender.send(message, success: { + DispatchQueue.main.async { + SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false) + self?.presentingViewController?.dismiss(animated: true, completion: nil) + } + }, failure: { error in + let message = TSErrorMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, failedMessageType: .groupCreationFailed) + message.save() + DispatchQueue.main.async { + SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false) + self?.presentingViewController?.dismiss(animated: true, completion: nil) + } + }) + } + } } } @@ -69,7 +125,8 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableV private extension NewClosedGroupVC { final class Cell : UITableViewCell { - var hexEncodedPublicKey: String = "" { didSet { update() } } + var hexEncodedPublicKey = "" { didSet { update() } } + var hasTick = false { didSet { update() } } // MARK: Components private lazy var profilePictureView = ProfilePictureView() @@ -82,7 +139,16 @@ private extension NewClosedGroupVC { return result }() - lazy var separator: UIView = { + private lazy var tickImageView: UIImageView = { + let result = UIImageView() + result.contentMode = .scaleAspectFit + let size: CGFloat = 24 + result.set(.width, to: size) + result.set(.height, to: size) + return result + }() + + private lazy var separator: UIView = { let result = UIView() result.backgroundColor = Colors.separator result.set(.height, to: Values.separatorThickness) @@ -113,7 +179,7 @@ private extension NewClosedGroupVC { profilePictureView.set(.height, to: profilePictureViewSize) profilePictureView.size = profilePictureViewSize // Set up the main stack view - let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel ]) + let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel, tickImageView ]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = Values.mediumSpacing @@ -121,21 +187,21 @@ private extension NewClosedGroupVC { contentView.addSubview(stackView) stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) stackView.pin(.top, to: .top, of: contentView, withInset: Values.mediumSpacing) - contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.mediumSpacing) contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.mediumSpacing) stackView.set(.width, to: UIScreen.main.bounds.width - 2 * Values.mediumSpacing) // Set up the separator addSubview(separator) separator.pin(.leading, to: .leading, of: self) - separator.pin(.trailing, to: .trailing, of: self) separator.pin(.bottom, to: .bottom, of: self) + separator.set(.width, to: UIScreen.main.bounds.width) } // MARK: Updating private func update() { - displayNameLabel.text = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? "Unknown Contact" profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey profilePictureView.update() + displayNameLabel.text = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? "Unknown Contact" + tickImageView.image = hasTick ? #imageLiteral(resourceName: "CircleCheck") : #imageLiteral(resourceName: "Circle") } } } diff --git a/Signal/src/UserInterface/Notifications/AppNotifications.swift b/Signal/src/UserInterface/Notifications/AppNotifications.swift index 94083e78a..8ad0c625f 100644 --- a/Signal/src/UserInterface/Notifications/AppNotifications.swift +++ b/Signal/src/UserInterface/Notifications/AppNotifications.swift @@ -371,8 +371,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { notificationTitle = senderName case is TSGroupThread: var groupName = thread.name() - if groupName.count < 1 { - groupName = MessageStrings.newGroupDefaultTitle + if groupName.count < 1 || groupName == "New Group" { + groupName = DisplayNameUtilities.getDisplayName(for: thread as! TSGroupThread) } notificationTitle = String(format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 7bfe0b557..6c5917f63 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2788,3 +2788,4 @@ "Would you like to restore your session with %@?" = "Would you like to restore your session with %@?"; "Restore" = "Restore"; "Dismiss" = "Dismiss"; +"New Closed Group" = "New Closed Group"; diff --git a/SignalServiceKit/src/Loki/Utilities/DisplayNameUtilities.swift b/SignalServiceKit/src/Loki/Utilities/DisplayNameUtilities.swift index ff41607f3..262be8037 100644 --- a/SignalServiceKit/src/Loki/Utilities/DisplayNameUtilities.swift +++ b/SignalServiceKit/src/Loki/Utilities/DisplayNameUtilities.swift @@ -1,4 +1,6 @@ +// TODO: Rename some of these functions to make the distinctions between them clearer. + @objc(LKDisplayNameUtilities) public final class DisplayNameUtilities : NSObject { @@ -12,6 +14,7 @@ public final class DisplayNameUtilities : NSObject { return SSKEnvironment.shared.profileManager.localProfileName() } + // MARK: Sessions @objc public static func getPrivateChatDisplayName(for hexEncodedPublicKey: String) -> String? { if hexEncodedPublicKey == userHexEncodedPublicKey { return userDisplayName @@ -20,6 +23,19 @@ public final class DisplayNameUtilities : NSObject { } } + // MARK: Closed Groups + @objc public static func getDisplayName(for group: TSGroupThread) -> String { + let members = group.groupModel.groupMemberIds + let displayNames = members.map { hexEncodedPublicKey -> String in + guard let displayName = DisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) else { return hexEncodedPublicKey } + let regex = try! NSRegularExpression(pattern: ".* \\(\\.\\.\\.[0-9a-fA-F]*\\)") + guard regex.hasMatch(input: displayName) else { return displayName } + return String(displayName[displayName.startIndex..<(displayName.index(displayName.endIndex, offsetBy: -14))]) + }.sorted() + return displayNames.joined(separator: ", ") + } + + // MARK: Open Groups @objc public static func getPublicChatDisplayName(for hexEncodedPublicKey: String, in channel: UInt64, on server: String) -> String? { var result: String? OWSPrimaryStorage.shared().dbReadConnection.read { transaction in