diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index fcabe56aa..a754d85b5 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -138,9 +138,9 @@ 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; - 7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; }; 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; + 7B8C44C528B49DDA00FBE25F /* NewConversationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; @@ -165,6 +165,7 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; }; 7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; }; 7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; }; + 7BB92B3F28C825FD0082762F /* NewConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */; }; 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */; }; 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */; }; 7BC01A42241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -1180,9 +1181,9 @@ 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; - 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = ""; }; 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; + 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationVC.swift; sourceTree = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; @@ -1207,6 +1208,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationViewModel.swift; sourceTree = ""; }; 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TappableLabel.swift; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; @@ -2212,6 +2214,7 @@ children = ( B8CCF63623961D6D0091D419 /* NewDMVC.swift */, 7B8C44C428B49DDA00FBE25F /* NewConversationVC.swift */, + 7BB92B3E28C825FD0082762F /* NewConversationViewModel.swift */, ); path = "New Conversation"; sourceTree = ""; @@ -5468,6 +5471,7 @@ B8544E3323D50E4900299F14 /* SNAppearance.swift in Sources */, 4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */, C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */, + 7BB92B3F28C825FD0082762F /* NewConversationViewModel.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */, C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */, diff --git a/Session/Home/New Conversation/NewConversationVC.swift b/Session/Home/New Conversation/NewConversationVC.swift index a7d00e555..4ba08e519 100644 --- a/Session/Home/New Conversation/NewConversationVC.swift +++ b/Session/Home/New Conversation/NewConversationVC.swift @@ -7,7 +7,7 @@ import SessionUIKit import SessionMessagingKit final class NewConversationVC: BaseVC, UITableViewDelegate, UITableViewDataSource { - private let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + private let newConversationViewModel = NewConversationViewModel() private var groupedContacts: OrderedDictionary = OrderedDictionary() // MARK: - UI @@ -48,7 +48,7 @@ final class NewConversationVC: BaseVC, UITableViewDelegate, UITableViewDataSourc let result = UILabel() result.textColor = Colors.text result.text = "Contacts" - result.font = .systemFont(ofSize: Values.mediumSpacing) + result.font = .systemFont(ofSize: Values.mediumFontSize) return result }() @@ -58,6 +58,9 @@ final class NewConversationVC: BaseVC, UITableViewDelegate, UITableViewDataSourc result.dataSource = self result.separatorStyle = .none result.backgroundColor = .clear + if #available(iOS 15.0, *) { + result.sectionHeaderTopPadding = 0 + } result.register(view: UserCell.self) return result @@ -74,47 +77,45 @@ final class NewConversationVC: BaseVC, UITableViewDelegate, UITableViewDataSourc closeButton.tintColor = Colors.text navigationItem.leftBarButtonItem = closeButton setUpViewHierarchy() - populateData() } private func setUpViewHierarchy() { buttonStackViewContainer.backgroundColor = Colors.cellBackground - view.addSubview(buttonStackViewContainer) - buttonStackViewContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: view) - buttonStackViewContainer.pin(.top, to: .top, of: view, withInset: Values.smallSpacing) - view.addSubview(contactsTitleLabel) - contactsTitleLabel.pin(.leading, to: .leading, of: view, withInset: Values.mediumSpacing) + let headerView = UIView( + frame: CGRect( + x: 0, y: 0, + width: UIScreen.main.bounds.width, + height: NewConversationButton.height * 3 + Values.smallSpacing * 2 + Values.mediumFontSize + ) + ) + headerView.addSubview(buttonStackViewContainer) + buttonStackViewContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: headerView) + headerView.addSubview(contactsTitleLabel) + contactsTitleLabel.pin(.leading, to: .leading, of: headerView, withInset: Values.mediumSpacing) contactsTitleLabel.pin(.top, to: .bottom, of: buttonStackViewContainer, withInset: Values.smallSpacing) + contactsTableView.tableHeaderView = headerView view.addSubview(contactsTableView) - contactsTableView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom], to: view) - contactsTableView.pin(.top, to: .bottom, of: contactsTitleLabel, withInset: Values.smallSpacing) - } - - private func populateData() { - contactProfiles.map{ profile in - let latinString = NSMutableString(string: profile.displayName()) - CFStringTransform(latinString, nil, kCFStringTransformToLatin, false) - CFStringTransform(latinString, nil, kCFStringTransformStripDiacritics, false) - } + contactsTableView.pin(to: view) } // MARK: - UITableViewDataSource func numberOfSections(in tableView: UITableView) -> Int { - return 1 + return newConversationViewModel.sectionData.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return contactProfiles.count + return newConversationViewModel.sectionData[section].contacts.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath) + let profile = newConversationViewModel.sectionData[indexPath.section].contacts[indexPath.row] cell.update( - with: contactProfiles[indexPath.row].id, - profile: contactProfiles[indexPath.row], + with: profile.id, + profile: profile, isZombie: false, accessory: .none ) @@ -122,14 +123,20 @@ final class NewConversationVC: BaseVC, UITableViewDelegate, UITableViewDataSourc } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return nil + let label = UILabel() + label.textColor = Colors.sessionMessageRequestsInfoText + label.font = .systemFont(ofSize: Values.smallFontSize) + label.text = newConversationViewModel.sectionData[section].sectionName + let headerView = UIView() + headerView.backgroundColor = self.view.backgroundColor + headerView.addSubview(label) + label.pin(.left, to: .left, of: headerView, withInset: Values.mediumSpacing) + label.pin(.top, to: .top, of: headerView, withInset: Values.verySmallSpacing) + label.pin(.bottom, to: .bottom, of: headerView, withInset: -Values.verySmallSpacing) + return headerView } // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 0 - } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -177,7 +184,7 @@ private final class NewConversationButton: UIView { private let title: String private let shouldShowSeparator: Bool - private static let height: CGFloat = 56 + public static let height: CGFloat = 56 private static let iconSize: CGFloat = 38 init(icon: UIImage, title: String, shouldShowSeparator: Bool = true) { diff --git a/Session/Home/New Conversation/NewConversationViewModel.swift b/Session/Home/New Conversation/NewConversationViewModel.swift new file mode 100644 index 000000000..db200413f --- /dev/null +++ b/Session/Home/New Conversation/NewConversationViewModel.swift @@ -0,0 +1,37 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import GRDB +import SessionMessagingKit + + +public class NewConversationViewModel { + struct SectionData { + var sectionName: String + var contacts: [Profile] + } + + let sectionData: [SectionData] + + init() { + let contactProfiles: [Profile] = Profile.fetchAllContactProfiles(excludeCurrentUser: true) + + var groupedContacts: [String: SectionData] = [:] + contactProfiles.forEach { profile in + let displayName = NSMutableString(string: profile.displayName()) + CFStringTransform(displayName, nil, kCFStringTransformToLatin, false) + CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false) + let section: String = displayName.substring(to: 1).capitalized.isSingleAlphabet ? + displayName.substring(to: 1).capitalized : + "#" + + if groupedContacts[section] == nil { + groupedContacts[section] = SectionData( + sectionName: section, + contacts: []) + } + groupedContacts[section]?.contacts.append(profile) + } + + sectionData = groupedContacts.values.sorted{ $0.sectionName < $1.sectionName } + } +} diff --git a/SessionUtilitiesKit/General/String+Utilities.swift b/SessionUtilitiesKit/General/String+Utilities.swift index 28dc50ac2..ab7ddc76c 100644 --- a/SessionUtilitiesKit/General/String+Utilities.swift +++ b/SessionUtilitiesKit/General/String+Utilities.swift @@ -9,6 +9,14 @@ public extension String { return CTLineGetGlyphCount(line) } + + var isSingleAlphabet: Bool { + return (glyphCount == 1 && isAlphabetic) + } + + var isAlphabetic: Bool { + return !isEmpty && range(of: "[^a-zA-Z]", options: .regularExpression) == nil + } var isSingleEmoji: Bool { return (glyphCount == 1 && containsEmoji)