smart list: implement a new design
Change-Id: Ifbdda250b0c831103980c88d068a2d59897952e4
diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index 9867ce3..057fcc6 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -242,10 +242,13 @@
2673D62E252624AB000C56CB /* ConferenceMenuItemsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2673D62D252624AB000C56CB /* ConferenceMenuItemsManager.swift */; };
2673D630252657B0000C56CB /* ConferenceLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2673D62F252657B0000C56CB /* ConferenceLayout.swift */; };
2673D63225267785000C56CB /* ConferenceLayoutHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2673D63125267785000C56CB /* ConferenceLayoutHelper.swift */; };
+ 267792882AC2100000759110 /* SmartListHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 267792872AC2100000759110 /* SmartListHeaderView.xib */; };
+ 2677928A2AC2103300759110 /* SmartListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267792892AC2103300759110 /* SmartListHeaderView.swift */; };
2679E42C29328352007E4639 /* ConcurrentDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2679E42B29328352007E4639 /* ConcurrentDictionary.swift */; };
267AD77E252B979F00047593 /* ConferenceParticipant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267AD77D252B979F00047593 /* ConferenceParticipant.swift */; };
267CC6FC29D5B27600505E56 /* Data+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260AE64229C8F58800D66D5E /* Data+Helpers.swift */; };
267CC6FD29D5B6D100505E56 /* VCardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1B54F1F75AD4700BA98EE /* VCardUtils.swift */; };
+ 267CFD6F2ACCDCD70066FE15 /* SmartListNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267CFD6E2ACCDCD70066FE15 /* SmartListNavigationBar.swift */; };
2682CC6D25110E36003E65E1 /* ContactPickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2682CC6C25110E36003E65E1 /* ContactPickerDelegate.swift */; };
268AA5C12472D42700B654A0 /* ConfirmationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 268AA5C02472D42700B654A0 /* ConfirmationAlert.swift */; };
26929FD425659E5F0052A601 /* cacert.pem in Resources */ = {isa = PBXBuildFile; fileRef = 26929FD125659E5F0052A601 /* cacert.pem */; };
@@ -854,8 +857,11 @@
2673D62D252624AB000C56CB /* ConferenceMenuItemsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceMenuItemsManager.swift; sourceTree = "<group>"; };
2673D62F252657B0000C56CB /* ConferenceLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceLayout.swift; sourceTree = "<group>"; };
2673D63125267785000C56CB /* ConferenceLayoutHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceLayoutHelper.swift; sourceTree = "<group>"; };
+ 267792872AC2100000759110 /* SmartListHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = SmartListHeaderView.xib; path = Ring/Features/Conversations/SmartList/SmartListHeaderView.xib; sourceTree = SOURCE_ROOT; };
+ 267792892AC2103300759110 /* SmartListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SmartListHeaderView.swift; path = Ring/Features/Conversations/SmartList/SmartListHeaderView.swift; sourceTree = SOURCE_ROOT; };
2679E42B29328352007E4639 /* ConcurrentDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentDictionary.swift; sourceTree = "<group>"; };
267AD77D252B979F00047593 /* ConferenceParticipant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConferenceParticipant.swift; sourceTree = "<group>"; };
+ 267CFD6E2ACCDCD70066FE15 /* SmartListNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartListNavigationBar.swift; sourceTree = "<group>"; };
2682CC6C25110E36003E65E1 /* ContactPickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPickerDelegate.swift; sourceTree = "<group>"; };
268AA5C02472D42700B654A0 /* ConfirmationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationAlert.swift; sourceTree = "<group>"; };
26929FD125659E5F0052A601 /* cacert.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cacert.pem; sourceTree = "<group>"; };
@@ -1514,6 +1520,7 @@
26B37337263C439B00E2EE28 /* CustomSearchController.swift */,
26B37341263C4B3700E2EE28 /* CustomSearchBar.swift */,
BB90263428F0918700B85859 /* PaddingTextField.swift */,
+ 267CFD6E2ACCDCD70066FE15 /* SmartListNavigationBar.swift */,
);
path = UI;
sourceTree = "<group>";
@@ -2018,6 +2025,8 @@
2662FC80246B793500FA7782 /* IncognitoSmartListViewController.storyboard */,
2662FC7C246B78E800FA7782 /* IncognitoSmartListViewController.swift */,
2662FC7E246B790400FA7782 /* IncognitoSmartListViewModel.swift */,
+ 267792872AC2100000759110 /* SmartListHeaderView.xib */,
+ 267792892AC2103300759110 /* SmartListHeaderView.swift */,
);
path = Smartlist;
sourceTree = "<group>";
@@ -2623,6 +2632,7 @@
0E6F545622403ADE00ECC3CE /* CreateSipAccountViewController.storyboard in Resources */,
1A2D18FD1F292DAD00B2C785 /* SmartListCell.xib in Resources */,
0E320D50224ADF840070B515 /* DialpadViewController.storyboard in Resources */,
+ 267792882AC2100000759110 /* SmartListHeaderView.xib in Resources */,
1ABE07DC1F0D915100D36361 /* Localizable.strings in Resources */,
1A2D18E61F29197100B2C785 /* MessageAccessoryView.xib in Resources */,
0E3BD4242044776300A50DDF /* ContactViewController.storyboard in Resources */,
@@ -2856,6 +2866,7 @@
56AC650E1E85694D00EA1AA9 /* DesignableTextField.swift in Sources */,
1A2D189A1F2642C000B2C785 /* NotificationCenter+Ring.swift in Sources */,
1DF75AC4296E0A940055EA87 /* View+Helpers.swift in Sources */,
+ 267CFD6F2ACCDCD70066FE15 /* SmartListNavigationBar.swift in Sources */,
BB06BD8329D492AA0064F0FC /* LocationSharingVM.swift in Sources */,
2662FC79246B1E1700FA7782 /* JamiSearchView.swift in Sources */,
0E44B62F202B9DE40060F71B /* LocalNotificationsHelper.swift in Sources */,
@@ -2877,6 +2888,7 @@
0ECA5683243394960055D31E /* MigrateAccountViewController.swift in Sources */,
BB3AB06C29316FA6006906BA /* ViewDidLoadModifier.swift in Sources */,
1A2041821F1E906B00C08435 /* CreateProfileViewModel.swift in Sources */,
+ 2677928A2AC2103300759110 /* SmartListHeaderView.swift in Sources */,
1A0C4EE31F1D673600550433 /* InjectionBag.swift in Sources */,
0E3BD4262044778100A50DDF /* ContactViewModel.swift in Sources */,
0EF49A9F23828C960064CD98 /* ConferenceParticipantView.swift in Sources */,
diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift
index c8e9488..3955122 100644
--- a/Ring/Ring/Constants/Generated/Strings.swift
+++ b/Ring/Ring/Constants/Generated/Strings.swift
@@ -648,8 +648,8 @@
internal static let noResults = L10n.tr("Localizable", "smartlist.noResults", fallback: "No results")
/// Public Directory
internal static let results = L10n.tr("Localizable", "smartlist.results", fallback: "Public Directory")
- /// Search for new or existing contact...
- internal static let searchBar = L10n.tr("Localizable", "smartlist.searchBar", fallback: "Search for new or existing contact...")
+ /// Search
+ internal static let searchBar = L10n.tr("Localizable", "smartlist.searchBar", fallback: "Search")
/// Enter name...
internal static let searchBarPlaceholder = L10n.tr("Localizable", "smartlist.searchBarPlaceholder", fallback: "Enter name...")
/// Select one of the numbers
diff --git a/Ring/Ring/Extensions/UIColor+Ring.swift b/Ring/Ring/Extensions/UIColor+Ring.swift
index 6957a09..52d8456 100644
--- a/Ring/Ring/Extensions/UIColor+Ring.swift
+++ b/Ring/Ring/Extensions/UIColor+Ring.swift
@@ -87,7 +87,7 @@
self.init(red: red, green: green, blue: blue, alpha: 1)
}
- static let jamiMain = UIColor(hex: 0x3F6DA7, alpha: 1.0)
+ static let jamiMain = UIColor(red: 0, green: 86, blue: 153, alpha: 1.0)
static let conferenceRaiseHand = UIColor(red: 0, green: 184, blue: 255, alpha: 1.0)
static let jamiSecondary = UIColor(hex: 0x1F4971, alpha: 1.0)
static let jamiButtonLight = UIColor(named: "jamiButtonLight")!
diff --git a/Ring/Ring/Extensions/UIImage+Helpers.swift b/Ring/Ring/Extensions/UIImage+Helpers.swift
index 9b21f3c..c3629e0 100644
--- a/Ring/Ring/Extensions/UIImage+Helpers.swift
+++ b/Ring/Ring/Extensions/UIImage+Helpers.swift
@@ -265,30 +265,70 @@
return image
}
- class func defaultJamiAvatarFor(profileName: String?, account: AccountModel?) -> UIImage {
- guard let account = account else { return UIImage(asset: Asset.icContactPicture)! }
- let image = UIImage(asset: Asset.icContactPicture)!
- .withAlignmentRectInsets(UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4))
- var name: String? = (profileName != nil) ? profileName :
- !account.registeredName.isEmpty ?
- account.registeredName : nil
- if let userNameData = UserDefaults.standard.dictionary(forKey: registeredNamesKey),
- let accountName = userNameData[account.id] as? String,
- !accountName.isEmpty {
- name = accountName
+ func fillJamiBackgroundColor() -> UIImage {
+ let color = UIColor.jamiMain
+ let inset: CGFloat = 4
+ let newSize = CGSize(width: self.size.width + 2 * inset, height: self.size.height + 2 * inset)
+ let drawingRect = CGRect(x: inset, y: inset, width: self.size.width, height: self.size.height)
+
+ UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
+
+ let context = UIGraphicsGetCurrentContext()
+
+ color.setFill()
+ context?.fill(CGRect(origin: CGPoint.zero, size: newSize))
+
+ UIColor.white.setFill()
+
+ self.withRenderingMode(.alwaysTemplate).draw(in: drawingRect)
+
+ let imageWithBackground = UIGraphicsGetImageFromCurrentImageContext()
+
+ UIGraphicsEndImageContext()
+
+ return imageWithBackground ?? self
+ }
+
+ class func defaultJamiAvatarFor(profileName: String?, account: AccountModel?, size: CGFloat) -> UIImage {
+ func generateDefaultImage() -> UIImage {
+ let configuration = UIImage.SymbolConfiguration(pointSize: size, weight: .regular, scale: .medium)
+ let defaultImage = UIImage(systemName: "person.fill", withConfiguration: configuration)
+ return defaultImage?.fillJamiBackgroundColor().circleMasked ?? defaultImage!.fillJamiBackgroundColor()
}
- guard let username = name else { return image }
- let scanner = Scanner(string: username.toMD5HexString().prefixString())
- var index: UInt64 = 0
- if scanner.scanHexInt64(&index) {
- let fbaBGColor = avatarColors[Int(index)]
- if !username.isSHA1() && !username.isEmpty {
- if let avatar = image.drawText(text: username.prefixString().capitalized, backgroundColor: fbaBGColor, textColor: UIColor.white, size: CGSize(width: 40, height: 40)) {
- return avatar
- }
+
+ func extractUsername(from profileName: String?, and account: AccountModel?) -> String? {
+ if let profileName = profileName, !profileName.isEmpty {
+ return profileName
+ } else if let accountName = account?.registeredName, !accountName.isEmpty {
+ return accountName
+ } else if let accountID = account?.id,
+ let userNameData = UserDefaults.standard.dictionary(forKey: registeredNamesKey),
+ let accountName = userNameData[accountID] as? String, !accountName.isEmpty {
+ return accountName
}
+ return nil
}
- return image
+
+ func generateAvatar(from username: String) -> UIImage? {
+ let scanner = Scanner(string: username.toMD5HexString().prefixString())
+ var index: UInt64 = 0
+
+ guard scanner.scanHexInt64(&index) else { return nil }
+ let fbaBGColor = avatarColors[Int(index)]
+
+ if !username.isSHA1() && !username.isEmpty {
+ return UIImage().drawText(text: username.prefixString().capitalized,
+ backgroundColor: fbaBGColor,
+ textColor: .white,
+ size: CGSize(width: size + 8, height: size + 8))?.circleMasked
+ }
+ return nil
+ }
+
+ let defaultImage = generateDefaultImage()
+ guard let account = account else { return defaultImage }
+ guard let username = extractUsername(from: profileName, and: account) else { return defaultImage }
+ return generateAvatar(from: username) ?? defaultImage
}
class func mergeImages(image1: UIImage, image2: UIImage, spacing: CGFloat = 6, height: CGFloat) -> UIImage {
@@ -406,7 +446,6 @@
UIGraphicsBeginImageContextWithOptions(self.size, false, UIScreen.main.scale)
if let context = UIGraphicsGetCurrentContext() {
let rect = CGRect(origin: .zero, size: size)
- // context.setFillColor(color.cgColor)
self.draw(in: rect)
context.setBlendMode(CGBlendMode.normal)
context.setFillColor(color.cgColor)
diff --git a/Ring/Ring/Extensions/UIViewController+Ring.swift b/Ring/Ring/Extensions/UIViewController+Ring.swift
index 85c348b..99f3611 100644
--- a/Ring/Ring/Extensions/UIViewController+Ring.swift
+++ b/Ring/Ring/Extensions/UIViewController+Ring.swift
@@ -156,7 +156,48 @@
.disposed(by: disposeBag)
}
- func configureRingNavigationBar() {
+ func configureLargeTitleNavigationBar() {
+ let appearance = UINavigationBarAppearance()
+ appearance.configureWithOpaqueBackground()
+ let titleFont = UIFont.systemFont(ofSize: 17, weight: .medium)
+ appearance.titleTextAttributes = [.font: titleFont]
+
+ let largeTitleFont = UIFont.systemFont(ofSize: 34, weight: .medium)
+ appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.jamiMain, .font: largeTitleFont]
+
+ appearance.shadowColor = .clear
+ appearance.shadowImage = UIImage()
+
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ navigationController?.navigationBar.standardAppearance = appearance
+ navigationController?.navigationBar.prefersLargeTitles = true
+ self.navigationController?.navigationBar.layer.shadowOpacity = 0
+ self.navigationController?.navigationBar.tintColor = .label
+ }
+
+ func configureNavigationBar(isTransparent: Bool = false) {
+ let appearance = UINavigationBarAppearance()
+ if isTransparent {
+ appearance.configureWithTransparentBackground()
+ } else {
+ appearance.configureWithDefaultBackground()
+ appearance.backgroundColor = UIColor.systemBackground
+ }
+
+ // Explicitly set shadow properties to none
+ appearance.shadowColor = nil
+ appearance.shadowImage = nil
+ appearance.backgroundImage = nil
+
+ let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.jamiMain]
+ appearance.titleTextAttributes = textAttributes
+ navigationController?.navigationBar.tintColor = UIColor.jamiMain
+
+ // Apply the appearance configuration
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ navigationController?.navigationBar.standardAppearance = appearance
+
+ // Set other properties
self.navigationController?.navigationBar.barStyle = .default
self.navigationController?.navigationBar.isTranslucent = true
self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
@@ -164,16 +205,12 @@
self.navigationController?.navigationBar.layer.shadowOpacity = 0.1
self.navigationController?.navigationBar.layer.shadowRadius = 2
self.navigationController?.navigationBar.layer.masksToBounds = false
- self.navigationController?.navigationBar.shadowImage = UIImage()
- let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.jamiMain]
- self.navigationController?.navigationBar.titleTextAttributes = textAttributes
- self.navigationController?.navigationBar.tintColor = UIColor.jamiMain
- self.navigationController?.navigationBar.barTintColor = UIColor.systemBackground
#if swift(>=5.7)
if #available(iOS 16.0, *) {
navigationItem.preferredSearchBarPlacement = .stacked
}
#endif
+ navigationController?.navigationBar.prefersLargeTitles = false
}
func configureWalkrhroughNavigationBar() {
diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift
index 54c06b8..36c41c7 100644
--- a/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift
+++ b/Ring/Ring/Features/ContactRequests/ContactRequestsViewController.swift
@@ -44,7 +44,7 @@
noInvitationsPlaceholder.backgroundColor = UIColor.jamiBackgroundColor
noRequestsLabel.backgroundColor = UIColor.jamiBackgroundColor
noRequestsLabel.textColor = UIColor.jamiLabelColor
- self.configureRingNavigationBar()
+ self.configureLargeTitleNavigationBar()
self.tableView.rx.modelSelected(RequestItem.self)
.subscribe({ [weak self] item in
guard let self = self else { return }
diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
index a870051..ab9a3db 100644
--- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift
@@ -84,7 +84,7 @@
override func viewDidLoad() {
super.viewDidLoad()
messageAccessoryView.delegate = self
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
if #available(iOS 15.0, *) {
self.currentCallButton.isHidden = true
self.currentCallLabel.isHidden = true
@@ -123,7 +123,7 @@
self.navigationItem.rightBarButtonItems = []
self.navigationItem.setHidesBackButton(true, animated: false)
} else {
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.setRightNavigationButtons()
self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value,
displayName: self.viewModel.displayName.value,
@@ -180,8 +180,6 @@
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- self.navigationController?.navigationBar.setBackgroundImage(nil, for: UIBarMetrics.default)
- self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value,
displayName: self.viewModel.displayName.value,
username: self.viewModel.userName.value)
diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
index 148e2a6..5c3fce7 100644
--- a/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/MessageSwiftUI/ViewModels/MessagesListVM.swift
@@ -191,7 +191,7 @@
.subscribe(onNext: { [weak self] profile in
guard let self = self else { return }
let account = self.accountService.getAccount(fromAccountId: self.conversation.accountId)
- let defaultAvatar = UIImage.defaultJamiAvatarFor(profileName: profile.alias, account: account)
+ let defaultAvatar = UIImage.defaultJamiAvatarFor(profileName: profile.alias, account: account, size: 16)
if let photo = profile.photo,
let data = NSData(base64Encoded: photo,
options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
diff --git a/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationUI.swift b/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationUI.swift
index 8b4c926..ce0382d 100644
--- a/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationUI.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationUI.swift
@@ -66,6 +66,8 @@
var body: some View {
VStack(alignment: .leading) {
+ Spacer()
+ .frame(height: 10)
HStack(alignment: .center, spacing: 10) {
Button(action: {
self.hideKeyboard()
diff --git a/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationViewController.swift
index 307fed6..7b7cce4 100644
--- a/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationViewController.swift
+++ b/Ring/Ring/Features/Conversations/Conversation/SwarmCreationModel/SwarmCreationViewController.swift
@@ -54,12 +54,12 @@
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.setupSearchBar()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
+ self.navigationController?.navigationBar.layer.shadowColor = UIColor.clear.cgColor
self.navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .medium),
NSAttributedString.Key.foregroundColor: UIColor.jamiLabelColor]
diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift
index f6b5e04..0feb6c4 100644
--- a/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift
@@ -79,18 +79,12 @@
.map({ [weak self] accountProfile in
if let photo = accountProfile.photo,
let data = NSData(base64Encoded: photo,
- options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
- return UIImage(data: data) ?? UIImage(asset: Asset.fallbackAvatar)!
+ options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data?,
+ let image = UIImage(data: data) {
+ return image
}
- guard let account = self?.items[row].account else {
- return UIImage(asset: Asset.fallbackAvatar)!
- }
- guard let name = accountProfile.alias else {
- return UIImage.defaultJamiAvatarFor(profileName: nil,
- account: account)}
- let profileName = name.isEmpty ? nil : name
- return UIImage.defaultJamiAvatarFor(profileName: profileName,
- account: account)
+ let account = self?.items[row].account
+ return UIImage.defaultJamiAvatarFor(profileName: accountProfile.alias, account: account, size: 30)
})
.bind(to: accountView.avatarView.rx.image)
.disposed(by: DisposeBag())
diff --git a/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift
index a9b293e..728d3e6 100644
--- a/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/IncognitoSmartListViewController.swift
@@ -42,7 +42,7 @@
override func viewDidLoad() {
super.viewDidLoad()
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.setupSearchBar()
searchView.configure(with: viewModel.injectionBag, source: viewModel, isIncognito: true, delegate: viewModel)
self.setupUI()
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.swift b/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.swift
new file mode 100644
index 0000000..04e3910
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.swift
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+import UIKit
+import RxSwift
+
+class SmartListHeaderView: UIView {
+
+ @IBOutlet weak var conversationsSegmentControl: UISegmentedControl!
+
+ let conversationTitle = L10n.Smartlist.conversations
+ let requestsTitle = L10n.Smartlist.invitations
+
+ let horizontalPadding: CGFloat = 10.0
+ let verticalPadding: CGFloat = 5.0
+ let textFontSize: CGFloat = 14
+ let numberFontSize: CGFloat = 10
+ let disposeBag = DisposeBag()
+
+ func setUnread(messages: Int, requests: Int) {
+ if requests == 0 {
+ conversationsSegmentControl.setImage(nil, forSegmentAt: 0)
+ conversationsSegmentControl.setImage(nil, forSegmentAt: 1)
+ conversationsSegmentControl.isHidden = true
+ return
+ }
+ conversationsSegmentControl.isHidden = false
+ let attributedMessages = createAttributedString(text: conversationTitle, number: messages)
+ conversationsSegmentControl.setImage(getImageForSegment(attributedString: attributedMessages)?.withRenderingMode(.alwaysOriginal), forSegmentAt: 0)
+ let attributedRequests = createAttributedString(text: requestsTitle, number: requests)
+ conversationsSegmentControl.setImage(getImageForSegment(attributedString: attributedRequests)?.withRenderingMode(.alwaysOriginal), forSegmentAt: 1)
+ }
+
+ func createAttributedString(text: String, number: Int) -> NSMutableAttributedString {
+ let baseString = "\(text) "
+ let font = UIFont.systemFont(ofSize: textFontSize)
+ let attributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: UIColor.label,
+ .font: font
+ ]
+
+ let attributedString = NSMutableAttributedString(string: baseString, attributes: attributes)
+ // Append the messages number
+ if number > 0 {
+ attributedString.append(roundedRectangleWithNumber(number))
+ }
+
+ return attributedString
+ }
+
+ func roundedRectangleWithNumber(_ number: Int) -> NSAttributedString {
+ let numberString = "\(number)"
+ let attributes: [NSAttributedString.Key: Any] = [
+ .font: UIFont.systemFont(ofSize: numberFontSize, weight: .medium),
+ .foregroundColor: UIColor.white
+ ]
+ let sizeOfString = numberString.size(withAttributes: attributes)
+ let roundedRectSize = CGSize(width: sizeOfString.width + horizontalPadding, height: sizeOfString.height + verticalPadding)
+
+ UIGraphicsBeginImageContextWithOptions(roundedRectSize, false, UIScreen.main.scale)
+
+ let roundedRectPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: roundedRectSize), cornerRadius: roundedRectSize.height / 2)
+ UIColor.jamiMain.setFill()
+ roundedRectPath.fill()
+
+ let stringOrigin = CGPoint(x: (roundedRectSize.width - sizeOfString.width) / 2, y: (roundedRectSize.height - sizeOfString.height) / 2)
+ numberString.draw(at: stringOrigin, withAttributes: attributes)
+
+ let roundedRectImage = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+
+ let attachment = NSTextAttachment()
+ attachment.image = roundedRectImage
+
+ attachment.bounds = CGRect(origin: CGPoint(x: 0, y: -3.5), size: roundedRectSize)
+
+ return NSAttributedString(attachment: attachment)
+ }
+
+ func getImageForSegment(attributedString: NSAttributedString) -> UIImage? {
+ let size = attributedString.size()
+ let paddedSize = CGSize(width: size.width + horizontalPadding, height: size.height + verticalPadding)
+ UIGraphicsBeginImageContextWithOptions(paddedSize, false, UIScreen.main.scale)
+
+ let context = UIGraphicsGetCurrentContext()
+ context?.setFillColor(UIColor.clear.cgColor)
+ context?.fill(CGRect(origin: .zero, size: paddedSize))
+
+ attributedString.draw(in: CGRect(origin: CGPoint(x: horizontalPadding * 0.5, y: verticalPadding * 0.5), size: size))
+
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+
+ return image
+ }
+}
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.xib b/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.xib
new file mode 100644
index 0000000..773f860
--- /dev/null
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartListHeaderView.xib
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
+ <device id="retina6_12" orientation="portrait" appearance="light"/>
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <objects>
+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+ <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="iN0-l3-epB" customClass="SmartListHeaderView" customModule="Ring" customModuleProvider="target">
+ <rect key="frame" x="0.0" y="0.0" width="393" height="40"/>
+ <subviews>
+ <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="eqj-XA-HbT">
+ <rect key="frame" x="0.0" y="0.0" width="393" height="40"/>
+ <subviews>
+ <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="pcM-dD-LRA">
+ <rect key="frame" x="0.0" y="0.0" width="393" height="41"/>
+ <segments>
+ <segment title="First"/>
+ <segment title="Second"/>
+ </segments>
+ </segmentedControl>
+ </subviews>
+ <constraints>
+ <constraint firstAttribute="trailing" secondItem="pcM-dD-LRA" secondAttribute="trailing" id="6rI-uU-lLp"/>
+ <constraint firstItem="pcM-dD-LRA" firstAttribute="leading" secondItem="eqj-XA-HbT" secondAttribute="leading" id="i1o-19-cbB"/>
+ </constraints>
+ </stackView>
+ </subviews>
+ <viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
+ <constraints>
+ <constraint firstAttribute="bottom" secondItem="eqj-XA-HbT" secondAttribute="bottom" id="0iV-De-DjM"/>
+ <constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="eqj-XA-HbT" secondAttribute="trailing" id="8jy-rq-2cT"/>
+ <constraint firstItem="eqj-XA-HbT" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="OrD-sV-vJH"/>
+ <constraint firstItem="eqj-XA-HbT" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="uDn-3H-3ya"/>
+ </constraints>
+ <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
+ <connections>
+ <outlet property="conversationsSegmentControl" destination="pcM-dD-LRA" id="YBy-tH-Xk6"/>
+ </connections>
+ <point key="canvasLocation" x="-79" y="20"/>
+ </view>
+ </objects>
+</document>
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
index 6fb52e1..2ca0dcd 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.storyboard
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Raw-Ee-7sK">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Raw-Ee-7sK">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
- <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
- <capability name="Named colors" minToolsVersion="9.0"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -13,261 +13,141 @@
<scene sceneID="oD9-xn-mt7">
<objects>
<viewController id="Raw-Ee-7sK" customClass="SmartlistViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController">
- <layoutGuides>
- <viewControllerLayoutGuide type="top" id="sbJ-yn-t3e"/>
- <viewControllerLayoutGuide type="bottom" id="cfq-zl-uux"/>
- </layoutGuides>
- <view key="view" contentMode="scaleToFill" id="2dZ-8A-4nq">
+ <view key="view" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2dZ-8A-4nq">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
- <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
<subviews>
- <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="fHs-UQ-egN">
- <rect key="frame" x="0.0" y="-45.5" width="375" height="712.5"/>
+ <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="uHv-0y-Afe">
+ <rect key="frame" x="0.0" y="78.5" width="375" height="588.5"/>
<subviews>
- <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="e5o-cY-djH" userLabel="Network Alert View">
- <rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
- <subviews>
- <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="HKv-H1-GYI" userLabel="Alert Labels View">
- <rect key="frame" x="171" y="0.0" width="33" height="56"/>
- <subviews>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fu7-Dr-XvA" userLabel="Network Alert Label">
- <rect key="frame" x="-2" y="10" width="37.5" height="18"/>
- <fontDescription key="fontDescription" type="system" pointSize="15"/>
- <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
- <nil key="highlightedColor"/>
- </label>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dHy-gp-i6K" userLabel="Cellular Alert Label">
- <rect key="frame" x="0.0" y="28" width="33" height="16"/>
- <fontDescription key="fontDescription" type="system" pointSize="13"/>
- <color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
- <nil key="highlightedColor"/>
- </label>
- </subviews>
- <constraints>
- <constraint firstItem="Fu7-Dr-XvA" firstAttribute="top" secondItem="HKv-H1-GYI" secondAttribute="top" constant="10" id="SAj-hd-YiO"/>
- <constraint firstItem="Fu7-Dr-XvA" firstAttribute="centerX" secondItem="HKv-H1-GYI" secondAttribute="centerX" id="apx-hI-eg9"/>
- <constraint firstAttribute="height" constant="56" id="j6O-zU-YVL"/>
- <constraint firstAttribute="width" secondItem="dHy-gp-i6K" secondAttribute="width" id="mfY-pK-Ush"/>
- <constraint firstItem="dHy-gp-i6K" firstAttribute="centerX" secondItem="HKv-H1-GYI" secondAttribute="centerX" id="orq-Lq-Fmo"/>
- <constraint firstItem="dHy-gp-i6K" firstAttribute="top" secondItem="Fu7-Dr-XvA" secondAttribute="bottom" id="pOy-bX-Jpj"/>
- </constraints>
- </view>
- <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="iaz-fd-fEz" userLabel="Settings Button">
- <rect key="frame" x="220" y="16" width="24" height="24"/>
- <accessibility key="accessibilityConfiguration">
- <accessibilityTraits key="traits" button="YES" image="YES"/>
- </accessibility>
- <constraints>
- <constraint firstAttribute="height" constant="24" id="d6g-Zu-vR7"/>
- <constraint firstAttribute="width" constant="24" id="nbF-R8-5kY"/>
- </constraints>
- <state key="normal" image="settings_icon"/>
- </button>
- </subviews>
- <color key="backgroundColor" red="0.94117647058823528" green="0.4392156862745098" blue="0.4392156862745098" alpha="1" colorSpace="calibratedRGB"/>
- <constraints>
- <constraint firstItem="iaz-fd-fEz" firstAttribute="leading" secondItem="HKv-H1-GYI" secondAttribute="trailing" constant="16" id="AFY-4C-aBu"/>
- <constraint firstItem="HKv-H1-GYI" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="e5o-cY-djH" secondAttribute="leading" constant="10" id="QoG-HF-kux"/>
- <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="iaz-fd-fEz" secondAttribute="trailing" constant="10" id="VGV-dn-yaS"/>
- <constraint firstItem="HKv-H1-GYI" firstAttribute="centerY" secondItem="e5o-cY-djH" secondAttribute="centerY" id="jtZ-C0-Lms"/>
- <constraint firstAttribute="height" constant="56" id="qrO-B9-Qmw"/>
- <constraint firstItem="iaz-fd-fEz" firstAttribute="centerY" secondItem="e5o-cY-djH" secondAttribute="centerY" id="xx8-oG-TVU"/>
- <constraint firstItem="HKv-H1-GYI" firstAttribute="centerX" secondItem="e5o-cY-djH" secondAttribute="centerX" priority="750" id="yI5-9z-dUv"/>
- </constraints>
- </view>
- <view alpha="0.90000000000000002" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ESS-lr-aeE">
- <rect key="frame" x="15" y="71" width="345" height="30"/>
- <subviews>
- <segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="VCI-Uk-W6P">
- <rect key="frame" x="0.0" y="0.0" width="345" height="31"/>
- <constraints>
- <constraint firstAttribute="height" constant="30" id="15y-jg-SBq"/>
- </constraints>
- <segments>
- <segment title="Conversations"/>
- <segment title="Requests"/>
- </segments>
- </segmentedControl>
- <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZhT-xd-SGe">
- <rect key="frame" x="100" y="7.5" width="30" height="15"/>
- <color key="backgroundColor" name="row_selected"/>
- <constraints>
- <constraint firstAttribute="height" constant="15" id="IjU-KN-JjW"/>
- </constraints>
- <fontDescription key="fontDescription" type="system" pointSize="11"/>
- <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
- <userDefinedRuntimeAttributes>
- <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
- </userDefinedRuntimeAttributes>
- </button>
- <button opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="7Ze-Hy-M2M">
- <rect key="frame" x="300" y="7.5" width="30" height="15"/>
- <color key="backgroundColor" name="row_selected"/>
- <constraints>
- <constraint firstAttribute="height" constant="15" id="lrx-8A-8cK"/>
- </constraints>
- <fontDescription key="fontDescription" type="system" pointSize="11"/>
- <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
- <userDefinedRuntimeAttributes>
- <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
- </userDefinedRuntimeAttributes>
- </button>
- </subviews>
+ <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="HFM-G6-hMN">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="588.5"/>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ </tableView>
+ <tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="opE-y7-3Rm">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="588.5"/>
+ <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ </tableView>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="uHt-ds-l8p">
+ <rect key="frame" x="187.5" y="294.5" width="0.0" height="0.0"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
- <constraints>
- <constraint firstItem="7Ze-Hy-M2M" firstAttribute="centerY" secondItem="ESS-lr-aeE" secondAttribute="centerY" id="2Yo-Ug-5ui"/>
- <constraint firstItem="ZhT-xd-SGe" firstAttribute="leading" secondItem="ESS-lr-aeE" secondAttribute="leading" constant="100" id="5VM-pc-lgH"/>
- <constraint firstAttribute="bottom" secondItem="VCI-Uk-W6P" secondAttribute="bottom" id="K2J-oQ-QF9"/>
- <constraint firstAttribute="trailing" secondItem="7Ze-Hy-M2M" secondAttribute="trailing" constant="15" id="USe-XU-NwW"/>
- <constraint firstItem="VCI-Uk-W6P" firstAttribute="top" secondItem="ESS-lr-aeE" secondAttribute="top" id="diC-bw-4ss"/>
- <constraint firstItem="ZhT-xd-SGe" firstAttribute="centerY" secondItem="ESS-lr-aeE" secondAttribute="centerY" id="gzI-va-Ime"/>
- <constraint firstItem="VCI-Uk-W6P" firstAttribute="leading" secondItem="ESS-lr-aeE" secondAttribute="leading" id="mu8-ki-TZu"/>
- <constraint firstAttribute="trailing" secondItem="VCI-Uk-W6P" secondAttribute="trailing" id="qWf-8F-nJK"/>
- </constraints>
- </view>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HGv-QU-VSD">
- <rect key="frame" x="162.5" y="116" width="50" height="20"/>
- <constraints>
- <constraint firstAttribute="height" constant="20" id="DDX-Se-AYW"/>
- </constraints>
- <fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
- <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="gqr-2R-GbC">
- <rect key="frame" x="0.0" y="151" width="375" height="561.5"/>
+ </subviews>
+ <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+ <constraints>
+ <constraint firstItem="opE-y7-3Rm" firstAttribute="top" secondItem="uHv-0y-Afe" secondAttribute="top" id="6oJ-4c-sHy"/>
+ <constraint firstItem="HFM-G6-hMN" firstAttribute="top" secondItem="uHv-0y-Afe" secondAttribute="top" id="ErI-lP-Clv"/>
+ <constraint firstItem="uHt-ds-l8p" firstAttribute="centerX" secondItem="uHv-0y-Afe" secondAttribute="centerX" id="GzM-zr-tQ3"/>
+ <constraint firstAttribute="bottom" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="IZp-Zd-9TF"/>
+ <constraint firstItem="uHt-ds-l8p" firstAttribute="centerY" secondItem="uHv-0y-Afe" secondAttribute="centerY" id="Izd-id-gKk"/>
+ <constraint firstAttribute="trailing" secondItem="HFM-G6-hMN" secondAttribute="trailing" id="J7y-5I-hiu"/>
+ <constraint firstItem="opE-y7-3Rm" firstAttribute="leading" secondItem="uHv-0y-Afe" secondAttribute="leading" id="KHo-YY-C3y"/>
+ <constraint firstAttribute="bottom" secondItem="opE-y7-3Rm" secondAttribute="bottom" id="Shr-dr-27u"/>
+ <constraint firstItem="HFM-G6-hMN" firstAttribute="leading" secondItem="uHv-0y-Afe" secondAttribute="leading" id="SxW-MG-L7u"/>
+ <constraint firstAttribute="trailing" secondItem="opE-y7-3Rm" secondAttribute="trailing" id="gbS-5H-tkZ"/>
+ </constraints>
+ </view>
+ <stackView opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" translatesAutoresizingMaskIntoConstraints="NO" id="Rda-vE-7T5">
+ <rect key="frame" x="0.0" y="20" width="375" height="58.5"/>
+ <subviews>
+ <label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="searching" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sy4-4D-ah4">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="40"/>
+ <constraints>
+ <constraint firstAttribute="height" constant="40" id="4tZ-iV-gfE"/>
+ </constraints>
+ <fontDescription key="fontDescription" type="system" pointSize="17"/>
+ <nil key="textColor"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fK7-Ou-K2i">
+ <rect key="frame" x="0.0" y="0.0" width="375" height="58.5"/>
<subviews>
- <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="HFM-G6-hMN">
- <rect key="frame" x="0.0" y="0.0" width="375" height="561.5"/>
- <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
- </tableView>
- <tableView hidden="YES" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="1" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="opE-y7-3Rm">
- <rect key="frame" x="0.0" y="0.0" width="375" height="561.5"/>
- <color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
- </tableView>
- <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="EvL-Bu-O1T">
- <rect key="frame" x="0.0" y="0.0" width="375" height="561.5"/>
+ <stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" translatesAutoresizingMaskIntoConstraints="NO" id="goP-9T-lJ1">
+ <rect key="frame" x="10" y="10" width="355" height="38.5"/>
<subviews>
- <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="No conversations" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8bB-XU-6gh">
- <rect key="frame" x="121.5" y="270.5" width="132.5" height="20.5"/>
- <fontDescription key="fontDescription" type="system" pointSize="17"/>
- <color key="textColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
- <nil key="highlightedColor"/>
- </label>
- </subviews>
- <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
- <constraints>
- <constraint firstItem="8bB-XU-6gh" firstAttribute="centerY" secondItem="EvL-Bu-O1T" secondAttribute="centerY" id="1R2-tE-dtX"/>
- <constraint firstItem="8bB-XU-6gh" firstAttribute="centerX" secondItem="EvL-Bu-O1T" secondAttribute="centerX" id="PCa-ph-Sbp"/>
- </constraints>
- </view>
- <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="79Q-fh-vhV">
- <rect key="frame" x="265" y="451.5" width="60" height="60"/>
- <subviews>
- <button opaque="NO" contentMode="scaleToFill" selected="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="k8G-Me-4BI">
- <rect key="frame" x="0.0" y="0.0" width="60" height="60"/>
+ <stackView opaque="NO" userInteractionEnabled="NO" contentMode="scaleToFill" axis="vertical" distribution="fillProportionally" alignment="center" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="3qc-Hq-jRW">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="38.5"/>
+ <subviews>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="749" text="No network connectivity" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gFu-6Z-dl9">
+ <rect key="frame" x="67.5" y="0.0" width="185" height="20.5"/>
+ <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
+ <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <nil key="highlightedColor"/>
+ </label>
+ <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Be sure cellular access is granted on your settings" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wJ3-e7-CMi">
+ <rect key="frame" x="8" y="22.5" width="304" height="16"/>
+ <fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
+ <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <nil key="highlightedColor"/>
+ </label>
+ </subviews>
+ <viewLayoutGuide key="safeArea" id="7kw-IK-33Z"/>
+ </stackView>
+ <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="aUA-Rj-5gi">
+ <rect key="frame" x="320" y="0.0" width="35" height="38.5"/>
<constraints>
- <constraint firstAttribute="width" constant="60" id="Dq9-B0-nTC"/>
- <constraint firstAttribute="height" constant="60" id="LCB-X4-TOf"/>
+ <constraint firstAttribute="width" constant="35" id="M1l-Hd-D5Z"/>
</constraints>
- <color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
- <size key="titleShadowOffset" width="-3" height="-3"/>
- <state key="normal" image="dialpad"/>
- <userDefinedRuntimeAttributes>
- <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
- <real key="value" value="30"/>
- </userDefinedRuntimeAttribute>
- <userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
- <real key="value" value="0.0"/>
- </userDefinedRuntimeAttribute>
- <userDefinedRuntimeAttribute type="boolean" keyPath="roundedCorners" value="YES"/>
- <userDefinedRuntimeAttribute type="color" keyPath="borderColor">
- <color key="value" red="0.80000000000000004" green="0.80000000000000004" blue="0.80000000000000004" alpha="0.3690336044520548" colorSpace="calibratedRGB"/>
- </userDefinedRuntimeAttribute>
- </userDefinedRuntimeAttributes>
+ <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <state key="normal" title="Button"/>
+ <buttonConfiguration key="configuration" style="plain" image="gearshape" catalog="system"/>
</button>
</subviews>
- <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+ <viewLayoutGuide key="safeArea" id="QVu-d0-XQT"/>
<constraints>
- <constraint firstAttribute="height" constant="60" id="GEI-dw-9ar"/>
- <constraint firstAttribute="trailing" secondItem="k8G-Me-4BI" secondAttribute="trailing" id="a7d-Q1-Fvz"/>
- <constraint firstAttribute="bottom" secondItem="k8G-Me-4BI" secondAttribute="bottom" id="bnM-1C-SSP"/>
- <constraint firstItem="k8G-Me-4BI" firstAttribute="top" secondItem="79Q-fh-vhV" secondAttribute="top" id="g5a-Yt-05m"/>
- <constraint firstAttribute="width" constant="60" id="hKy-2O-rN9"/>
- <constraint firstItem="k8G-Me-4BI" firstAttribute="leading" secondItem="79Q-fh-vhV" secondAttribute="leading" id="pqh-Ky-LOJ"/>
+ <constraint firstAttribute="height" constant="38.5" id="DvO-3i-ryD"/>
</constraints>
- <userDefinedRuntimeAttributes>
- <userDefinedRuntimeAttribute type="number" keyPath="cornerRadius">
- <real key="value" value="30"/>
- </userDefinedRuntimeAttribute>
- </userDefinedRuntimeAttributes>
- </view>
+ </stackView>
</subviews>
- <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+ <viewLayoutGuide key="safeArea" id="yx6-GK-nm1"/>
+ <color key="backgroundColor" red="0.89803922179999995" green="0.54509806630000002" blue="0.52549022440000004" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
<constraints>
- <constraint firstAttribute="trailing" secondItem="79Q-fh-vhV" secondAttribute="trailing" constant="50" id="3Cj-Ah-pWk"/>
- <constraint firstAttribute="bottom" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="Eig-nJ-OPf"/>
- <constraint firstItem="EvL-Bu-O1T" firstAttribute="top" secondItem="HFM-G6-hMN" secondAttribute="top" id="HwS-th-5Du"/>
- <constraint firstItem="EvL-Bu-O1T" firstAttribute="bottom" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="Krm-YJ-Rps"/>
- <constraint firstItem="opE-y7-3Rm" firstAttribute="bottom" secondItem="HFM-G6-hMN" secondAttribute="bottom" id="LnD-vv-oHS"/>
- <constraint firstItem="HFM-G6-hMN" firstAttribute="leading" secondItem="gqr-2R-GbC" secondAttribute="leading" id="MUp-ck-FM6"/>
- <constraint firstAttribute="trailing" secondItem="HFM-G6-hMN" secondAttribute="trailing" id="OgG-BG-rlJ"/>
- <constraint firstAttribute="trailing" secondItem="EvL-Bu-O1T" secondAttribute="trailing" id="RhB-ix-Yzq"/>
- <constraint firstItem="opE-y7-3Rm" firstAttribute="top" secondItem="HFM-G6-hMN" secondAttribute="top" id="Wvh-pw-hpY"/>
- <constraint firstAttribute="bottom" secondItem="79Q-fh-vhV" secondAttribute="bottom" constant="50" id="gTN-u6-c4o"/>
- <constraint firstItem="opE-y7-3Rm" firstAttribute="leading" secondItem="gqr-2R-GbC" secondAttribute="leading" id="jRB-q6-Xnc"/>
- <constraint firstItem="HFM-G6-hMN" firstAttribute="top" secondItem="gqr-2R-GbC" secondAttribute="top" id="m5h-zl-Bru"/>
- <constraint firstAttribute="trailing" secondItem="opE-y7-3Rm" secondAttribute="trailing" id="mZU-FB-mYU"/>
- <constraint firstItem="EvL-Bu-O1T" firstAttribute="leading" secondItem="gqr-2R-GbC" secondAttribute="leading" id="tCT-EK-NAi"/>
+ <constraint firstItem="goP-9T-lJ1" firstAttribute="leading" secondItem="fK7-Ou-K2i" secondAttribute="leading" constant="10" id="hTx-27-IZX"/>
+ <constraint firstAttribute="trailing" secondItem="goP-9T-lJ1" secondAttribute="trailing" constant="10" id="hnI-Kq-jjD"/>
+ <constraint firstAttribute="bottom" secondItem="goP-9T-lJ1" secondAttribute="bottom" constant="10" id="qbw-52-D1n"/>
+ <constraint firstItem="goP-9T-lJ1" firstAttribute="top" secondItem="fK7-Ou-K2i" secondAttribute="top" constant="10" id="qmt-fC-8L0"/>
</constraints>
</view>
</subviews>
- <constraints>
- <constraint firstItem="HFM-G6-hMN" firstAttribute="leading" secondItem="ESS-lr-aeE" secondAttribute="leading" constant="-15" id="Voi-fF-kql"/>
- <constraint firstItem="gqr-2R-GbC" firstAttribute="leading" secondItem="fHs-UQ-egN" secondAttribute="leading" id="XrR-qX-a55"/>
- <constraint firstAttribute="bottom" secondItem="gqr-2R-GbC" secondAttribute="bottom" id="aiq-Bo-nds"/>
- <constraint firstItem="HFM-G6-hMN" firstAttribute="trailing" secondItem="ESS-lr-aeE" secondAttribute="trailing" constant="15" id="dDg-sf-Ssc"/>
- <constraint firstItem="ESS-lr-aeE" firstAttribute="centerX" secondItem="fHs-UQ-egN" secondAttribute="centerX" id="klM-7A-Isg"/>
- <constraint firstAttribute="trailing" secondItem="gqr-2R-GbC" secondAttribute="trailing" id="w3Y-e4-ril"/>
- </constraints>
</stackView>
</subviews>
- <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+ <viewLayoutGuide key="safeArea" id="mnN-Gu-0lw"/>
+ <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
- <constraint firstAttribute="trailing" secondItem="fHs-UQ-egN" secondAttribute="trailing" id="IIq-iZ-FZu"/>
- <constraint firstItem="fHs-UQ-egN" firstAttribute="top" secondItem="2dZ-8A-4nq" secondAttribute="top" priority="250" id="YbK-2c-8Eo"/>
- <constraint firstItem="fHs-UQ-egN" firstAttribute="leading" secondItem="2dZ-8A-4nq" secondAttribute="leading" id="j1V-Rc-WhK"/>
- <constraint firstAttribute="bottom" secondItem="fHs-UQ-egN" secondAttribute="bottom" id="nI3-hX-UQv"/>
+ <constraint firstItem="Rda-vE-7T5" firstAttribute="top" secondItem="mnN-Gu-0lw" secondAttribute="top" id="8Jx-Gx-TIM"/>
+ <constraint firstItem="uHv-0y-Afe" firstAttribute="trailing" secondItem="mnN-Gu-0lw" secondAttribute="trailing" id="Jyx-v6-8PT"/>
+ <constraint firstItem="uHv-0y-Afe" firstAttribute="top" secondItem="mnN-Gu-0lw" secondAttribute="top" priority="750" id="hBy-Hp-Lsj"/>
+ <constraint firstItem="mnN-Gu-0lw" firstAttribute="trailing" secondItem="Rda-vE-7T5" secondAttribute="trailing" id="luu-Xu-KbJ"/>
+ <constraint firstItem="uHv-0y-Afe" firstAttribute="top" secondItem="Rda-vE-7T5" secondAttribute="bottom" id="sCv-Yt-MIH"/>
+ <constraint firstItem="Rda-vE-7T5" firstAttribute="leading" secondItem="mnN-Gu-0lw" secondAttribute="leading" id="sVq-4t-ORv"/>
+ <constraint firstItem="mnN-Gu-0lw" firstAttribute="bottom" secondItem="uHv-0y-Afe" secondAttribute="bottom" id="vCt-az-fDC"/>
+ <constraint firstItem="uHv-0y-Afe" firstAttribute="leading" secondItem="mnN-Gu-0lw" secondAttribute="leading" id="zbw-5B-TEY"/>
</constraints>
</view>
<extendedEdge key="edgesForExtendedLayout" top="YES"/>
<navigationItem key="navigationItem" id="zLl-0A-Dht"/>
<connections>
- <outlet property="cellularAlertLabel" destination="dHy-gp-i6K" id="W9w-Mi-GTY"/>
- <outlet property="containerView" destination="gqr-2R-GbC" id="xq5-ND-VjP"/>
- <outlet property="conversationBadge" destination="ZhT-xd-SGe" id="bIA-to-RWj"/>
- <outlet property="conversationBadgeLeadingConstraint" destination="5VM-pc-lgH" id="pin-4I-Rlk"/>
- <outlet property="conversationsSegmentControl" destination="VCI-Uk-W6P" id="tKl-bm-ywS"/>
+ <outlet property="cellularAlertLabel" destination="wJ3-e7-CMi" id="ScO-Xe-kfT"/>
+ <outlet property="containerView" destination="uHv-0y-Afe" id="qh0-9W-oy5"/>
<outlet property="conversationsTableView" destination="HFM-G6-hMN" id="M97-IB-NUZ"/>
- <outlet property="dialpadButton" destination="k8G-Me-4BI" id="Ij7-SF-nvZ"/>
- <outlet property="dialpadButtonShadow" destination="79Q-fh-vhV" id="VcA-wc-j6h"/>
- <outlet property="networkAlertLabel" destination="Fu7-Dr-XvA" id="0qV-lk-9mE"/>
- <outlet property="networkAlertView" destination="e5o-cY-djH" id="uV5-WT-vai"/>
- <outlet property="noConversationLabel" destination="8bB-XU-6gh" id="n4g-mz-w7z"/>
- <outlet property="noConversationsView" destination="EvL-Bu-O1T" id="tVV-6a-4Xg"/>
- <outlet property="requestBadgeTrailingConstraint" destination="USe-XU-NwW" id="iRW-Ph-bPH"/>
- <outlet property="requestsBadge" destination="7Ze-Hy-M2M" id="Re6-ef-Q8h"/>
+ <outlet property="networkAlertLabel" destination="gFu-6Z-dl9" id="NJF-MK-pWj"/>
+ <outlet property="networkAlertView" destination="fK7-Ou-K2i" id="amM-wV-HA8"/>
+ <outlet property="noConversationLabel" destination="uHt-ds-l8p" id="tYK-67-lg5"/>
<outlet property="searchView" destination="Y4B-5f-ij4" id="FtS-9R-atZ"/>
- <outlet property="segmentControlContainer" destination="ESS-lr-aeE" id="OlG-so-HqR"/>
- <outlet property="settingsButton" destination="iaz-fd-fEz" id="R2O-R8-BDk"/>
- <outlet property="viewContainer" destination="fHs-UQ-egN" id="lcd-Nd-7pC"/>
+ <outlet property="settingsButton" destination="aUA-Rj-5gi" id="CeY-yv-WaH"/>
+ <outlet property="tableTopConstraint" destination="sCv-Yt-MIH" id="xjO-1y-BVS"/>
+ <outlet property="widgetsTopConstraint" destination="8Jx-Gx-TIM" id="BRo-X5-pwM"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JSt-CJ-9Vq" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ <customObject id="MBy-7P-V3E" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target"/>
<customObject id="Y4B-5f-ij4" customClass="JamiSearchView" customModule="Ring" customModuleProvider="target">
<connections>
<outlet property="searchResultsTableView" destination="opE-y7-3Rm" id="cMo-3b-5FA"/>
- <outlet property="searchingLabel" destination="HGv-QU-VSD" id="WnL-0s-Szs"/>
+ <outlet property="searchingLabel" destination="sy4-4D-ah4" id="lMx-Yi-EwV"/>
</connections>
</customObject>
</objects>
@@ -275,14 +155,7 @@
</scene>
</scenes>
<resources>
- <image name="dialpad" width="37.5" height="37.5"/>
- <image name="settings_icon" width="24" height="24"/>
- <namedColor name="row_selected">
- <color red="0.81960784313725488" green="0.82352941176470584" blue="0.82352941176470584" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
- </namedColor>
- <systemColor name="groupTableViewBackgroundColor">
- <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
- </systemColor>
+ <image name="gearshape" catalog="system" width="128" height="123"/>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
index 6cba2f1..2892d9c 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewController.swift
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017-2021 Savoir-faire Linux Inc.
+ * Copyright (C) 2017-2023 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -33,67 +33,39 @@
struct SmartlistConstants {
static let smartlistRowHeight: CGFloat = 70.0
static let tableHeaderViewHeight: CGFloat = 30.0
- static let networkAllerHeight: CGFloat = 56.0
- static let tableViewOffset: CGFloat = 80.0
-
}
// swiftlint:disable type_body_length
-class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased {
+class SmartlistViewController: UIViewController, StoryboardBased, ViewModelBased, UISearchControllerDelegate {
private let log = SwiftyBeaver.self
// MARK: outlets
- @IBOutlet weak var viewContainer: UIView!
@IBOutlet weak var conversationsTableView: UITableView!
- @IBOutlet weak var conversationsSegmentControl: UISegmentedControl!
- @IBOutlet weak var segmentControlContainer: UIView!
- @IBOutlet weak var conversationBadge: UIButton!
- @IBOutlet weak var requestsBadge: UIButton!
- @IBOutlet weak var conversationBadgeLeadingConstraint: NSLayoutConstraint!
- @IBOutlet weak var requestBadgeTrailingConstraint: NSLayoutConstraint!
-
@IBOutlet weak var containerView: UIView!
- @IBOutlet weak var noConversationsView: UIView!
@IBOutlet weak var noConversationLabel: UILabel!
@IBOutlet weak var networkAlertLabel: UILabel!
@IBOutlet weak var cellularAlertLabel: UILabel!
- @IBOutlet weak var networkAlertViewTopConstraint: NSLayoutConstraint!
@IBOutlet weak var settingsButton: UIButton!
- @IBOutlet weak var dialpadButton: UIButton!
- @IBOutlet weak var dialpadButtonShadow: UIView!
+ @IBOutlet weak var tableTopConstraint: NSLayoutConstraint!
+ @IBOutlet weak var widgetsTopConstraint: NSLayoutConstraint!
@IBOutlet weak var networkAlertView: UIView!
@IBOutlet weak var searchView: JamiSearchView!
// account selection
- var accounPicker = UIPickerView()
- let accountPickerTextView = UITextField(frame: CGRect.zero)
- let accountsAdapter = AccountPickerAdapter()
- var accountsDismissTapRecognizer: UITapGestureRecognizer!
- var accountView = UIView()
- var accountWidth = NSLayoutConstraint()
- let nameLabelTag = 100
- let triangleTag = 200
- let openAccountTag = 300
- let margin: CGFloat = 10
- let size: CGFloat = 32
- let triangleViewSize: CGFloat = 12
+ private var accounPicker = UIPickerView()
+ private let accountPickerTextView = UITextField(frame: CGRect.zero)
+ private let accountsAdapter = AccountPickerAdapter()
+ private var accountsDismissTapRecognizer: UITapGestureRecognizer!
- var contactRequestVC: ContactRequestsViewController?
-
- // MARK: members
+ private var selectedSegmentIndex = BehaviorRelay<Int>(value: 0)
var viewModel: SmartlistViewModel!
private let disposeBag = DisposeBag()
private let contactPicker = CNContactPickerViewController()
+ private var headerView: SmartListHeaderView?
- // MARK: functions
- @IBAction func openScan() {
- self.viewModel.showQRCode()
- }
- @IBAction func createGroup() {
- self.viewModel.createGroup()
- }
+ private var contactRequestVC: ContactRequestsViewController?
override func viewDidLoad() {
super.viewDidLoad()
@@ -101,10 +73,9 @@
self.setupTableView()
self.setupUI()
self.applyL10n()
- self.configureRingNavigationBar()
+ self.configureLargeTitleNavigationBar()
self.confugureAccountPicker()
accountsDismissTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
-
/*
Register to keyboard notifications to adjust tableView insets when the keybaord appears
or disappears
@@ -113,25 +84,78 @@
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(withNotification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
self.setupSearchBar()
searchView.configure(with: viewModel.injectionBag, source: viewModel, isIncognito: false, delegate: viewModel)
- self.setUpContactRequest()
- conversationsSegmentControl.addTarget(self, action: #selector(segmentAction), for: .valueChanged)
+ if !self.viewModel.isSipAccount() {
+ self.setUpContactRequest()
+ }
}
- override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- // Waiting for screen size change
- DispatchQueue.global(qos: .background).async {
- sleep(UInt32(0.5))
- DispatchQueue.main.async { [weak self] in
- guard let self = self,
- UIDevice.current.portraitOrLandscape else { return }
- self.updateAccountItemSize()
- let messages: Int = Int(self.conversationBadge.title(for: .normal) ?? "0") ?? 0
- let requests: Int = Int(self.requestsBadge.title(for: .normal) ?? "0") ?? 0
- self.setUpSegmemtControl(messages: messages, requests: requests)
- self.searchController.sizeChanged(to: size.width, totalItems: 2.0)
- }
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ self.configureLargeTitleNavigationBar()
+ self.viewModel.closeAllPlayers()
+ self.navigationController?.setNavigationBarHidden(false, animated: false)
+ configureCustomNavBar(usingCustomSize: true)
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ configureCustomNavBar(usingCustomSize: false)
+ navigationController?.navigationBar.prefersLargeTitles = false
+ navigationController?.navigationBar.setNeedsLayout()
+ navigationController?.navigationBar.layoutIfNeeded()
+ }
+
+ func setupTableViewHeader(for tableView: UITableView) {
+ guard let headerView = loadHeaderView() else { return }
+
+ setupHeaderViewConstraints(headerView, in: tableView)
+ bindSegmentControlActions(headerView)
+ bindViewModelUpdates(to: headerView, in: tableView)
+ }
+
+ private func loadHeaderView() -> SmartListHeaderView? {
+ let nib = UINib(nibName: "SmartListHeaderView", bundle: nil)
+ return nib.instantiate(withOwner: nil, options: nil).first as? SmartListHeaderView
+ }
+
+ private func setupHeaderViewConstraints(_ headerView: SmartListHeaderView, in tableView: UITableView) {
+ tableView.tableHeaderView = headerView
+ NSLayoutConstraint.activate([
+ headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor, constant: -30),
+ headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor)
+ ])
+ }
+
+ private func bindSegmentControlActions(_ headerView: SmartListHeaderView) {
+ headerView.conversationsSegmentControl.addTarget(self, action: #selector(segmentAction), for: .valueChanged)
+
+ self.selectedSegmentIndex.subscribe { [weak headerView] index in
+ headerView?.conversationsSegmentControl.selectedSegmentIndex = index
}
- super.viewWillTransition(to: size, with: coordinator)
+ .disposed(by: self.disposeBag)
+ }
+
+ private func bindViewModelUpdates(to headerView: SmartListHeaderView, in tableView: UITableView) {
+ self.viewModel.updateSegmentControl
+ .subscribe { [weak headerView, weak tableView, weak self] (messages, requests) in
+ guard let headerView = headerView, let tableView = tableView else { return }
+
+ let height: CGFloat = requests == 0 ? 0 : 32
+ var frame = headerView.frame
+ frame.size.height = height
+ headerView.frame = frame
+
+ headerView.setUnread(messages: messages, requests: requests)
+
+ if requests == 0 {
+ headerView.conversationsSegmentControl.selectedSegmentIndex = 0
+ self?.navigationItem.title = L10n.Smartlist.conversations
+ }
+
+ // Resetting the header after adjusting its frame.
+ tableView.tableHeaderView = headerView
+ }
+ .disposed(by: self.disposeBag)
}
@objc
@@ -140,19 +164,22 @@
case 0:
contactRequestVC?.view.isHidden = true
searchView.showSearchResult = true
+ self.navigationItem.title = L10n.Smartlist.conversations
case 1:
contactRequestVC?.view.isHidden = false
searchView.showSearchResult = false
+ self.navigationItem.title = L10n.Smartlist.invitations
default:
break
}
+ self.selectedSegmentIndex.accept(segmentedControl.selectedSegmentIndex)
}
func addContactRequestVC(controller: ContactRequestsViewController) {
contactRequestVC = controller
}
- func setUpContactRequest() {
+ private func setUpContactRequest() {
guard let controller = contactRequestVC else { return }
addChild(controller)
@@ -163,6 +190,7 @@
// you must call this at the end per Apple's documentation
controller.didMove(toParent: self)
controller.view.isHidden = true
+ self.setupTableViewHeader(for: controller.tableView)
self.searchView.searchBar.rx.text.orEmpty
.debounce(Durations.textFieldThrottlingDuration.toTimeInterval(), scheduler: MainScheduler.instance)
.bind(to: (self.contactRequestVC?.viewModel.filter)!)
@@ -175,127 +203,38 @@
view.removeGestureRecognizer(accountsDismissTapRecognizer)
}
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
- self.navigationController?.navigationBar
- .titleTextAttributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue-Light", size: 25)!,
- NSAttributedString.Key.foregroundColor: UIColor.jamiMain]
- self.viewModel.closeAllPlayers()
- }
+ private func configureCustomNavBar(usingCustomSize: Bool) {
+ guard let customNavBar = navigationController?.navigationBar as? SmartListNavigationBar else { return }
- override func viewDidAppear(_ animated: Bool) {
- self.searchController.sizeChanged(to: self.view.frame.size.width, totalItems: 2.0)
- super.viewDidAppear(animated)
- viewContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15).isActive = true
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- self.navigationController?.setNavigationBarHidden(false, animated: false)
+ if usingCustomSize {
+ self.updateSearchBarIfActive()
+ customNavBar.usingCustomSize = true
+ } else {
+ customNavBar.removeTopView()
+ customNavBar.usingCustomSize = false
}
}
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- self.navigationController?.setNavigationBarHidden(false, animated: false)
- }
-
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating {
- if scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0 {
- self.navigationController?.setNavigationBarHidden(false, animated: true)
- } else {
- self.navigationController?.setNavigationBarHidden(true, animated: true)
- }
- }
- }
-
- func applyL10n() {
- noConversationLabel.text = L10n.Smartlist.noConversation
+ private func applyL10n() {
+ self.navigationItem.title = L10n.Smartlist.conversations
+ self.noConversationLabel.text = L10n.Smartlist.noConversation
self.networkAlertLabel.text = L10n.Smartlist.noNetworkConnectivity
self.cellularAlertLabel.text = L10n.Smartlist.cellularAccess
}
- func setUpSegmemtControl(messages: Int, requests: Int) {
- if requests == 0 {
- segmentControlContainer.isHidden = true
- conversationBadge.setTitle(String(messages), for: .normal)
- requestsBadge.setTitle(String(requests), for: .normal)
- return
- }
- if segmentControlContainer.isHidden {
- conversationsSegmentControl.selectedSegmentIndex = 0
- }
- segmentControlContainer.isHidden = false
- let unreadMessages = messages
- let unreadRequests = requests
- let margin: CGFloat = 5
-
- self.conversationBadge.isHidden = unreadMessages == 0
- self.requestsBadge.isHidden = unreadRequests == 0
- let titleFont = UIFont.systemFont(ofSize: 12, weight: .medium)
- let attributes = [NSAttributedString.Key.font: titleFont]
- let conversationTitle = L10n.Smartlist.conversations
- let requestsTitle = L10n.Smartlist.invitations
- conversationsSegmentControl.setTitleTextAttributes(attributes, for: .normal)
- conversationsSegmentControl.setTitle(conversationTitle, forSegmentAt: 0)
- conversationsSegmentControl.setTitle(requestsTitle, forSegmentAt: 1)
- conversationBadge.setTitle(String(unreadMessages), for: .normal)
- requestsBadge.setTitle(String(unreadRequests), for: .normal)
- self.conversationBadge.sizeToFit()
- self.requestsBadge.sizeToFit()
- self.conversationBadge.setNeedsDisplay()
- self.requestsBadge.setNeedsDisplay()
-
- let convBageWidth = unreadMessages == 0 ? 0 : conversationBadge.frame.width
- let requestBageWidth = unreadRequests == 0 ? 0 : requestsBadge.frame.size.width
- let widthConversation = conversationTitle.size(withAttributes: attributes).width
- let widthRequests = requestsTitle.size(withAttributes: attributes).width
-
- let segmentWidth = conversationsSegmentControl.frame.size.width * 0.5
- let convContentWidth = convBageWidth + widthConversation + margin
- let reqContentWidth = requestBageWidth + widthRequests + margin
- conversationsSegmentControl.setContentOffset(CGSize(width: -(convBageWidth + margin) * 0.5, height: 0), forSegmentAt: 0)
- conversationsSegmentControl.setContentOffset(CGSize(width: -(requestBageWidth + margin) * 0.5, height: 0), forSegmentAt: 1)
- let conversationConstraint = (segmentWidth - convContentWidth) * 0.5 + widthConversation + margin
- self.conversationBadgeLeadingConstraint.constant = conversationConstraint
- let requestConstraint = (segmentWidth - reqContentWidth) * 0.5
- self.requestBadgeTrailingConstraint.constant = requestConstraint
- }
-
- // swiftlint:disable function_body_length
- func setupUI() {
- conversationBadge.contentEdgeInsets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5)
- requestsBadge.contentEdgeInsets = UIEdgeInsets(top: 1, left: 5, bottom: 2, right: 5)
- conversationBadge.backgroundColor = UIColor.jamiMain
- requestsBadge.backgroundColor = UIColor.jamiMain
- self.viewModel.updateSegmentControl.subscribe { [weak self] (messages, requests) in
- self?.setUpSegmemtControl(messages: messages, requests: requests)
- }
- .disposed(by: self.disposeBag)
-
- view.backgroundColor = UIColor.jamiBackgroundColor
- conversationsTableView.backgroundColor = UIColor.jamiBackgroundColor
- noConversationsView.backgroundColor = UIColor.jamiBackgroundColor
- noConversationLabel.backgroundColor = UIColor.jamiBackgroundColor
- noConversationLabel.textColor = UIColor.jamiLabelColor
- dialpadButtonShadow.backgroundColor = UIColor.jamiBackgroundSecondaryColor
- dialpadButtonShadow.layer.shadowColor = UIColor.jamiLabelColor.cgColor
- dialpadButtonShadow.layer.shadowOffset = CGSize.zero
- dialpadButtonShadow.layer.shadowRadius = 1
- dialpadButtonShadow.layer.shadowOpacity = 0.6
- dialpadButtonShadow.layer.masksToBounds = false
+ private func setupUI() {
self.viewModel.hideNoConversationsMessage
- .bind(to: self.noConversationsView.rx.isHidden)
+ .bind(to: self.noConversationLabel.rx.isHidden)
.disposed(by: disposeBag)
- let isHidden = self.viewModel.networkConnectionState() == .none ? false : true
- self.networkAlertView.isHidden = isHidden
self.viewModel.connectionState
- .subscribe(onNext: { [weak self] connectionState in
- let isHidden = connectionState == .none ? false : true
- self?.networkAlertView.isHidden = isHidden
+ .startWith(self.viewModel.networkConnectionState())
+ .subscribe(onNext: { [weak self] _ in
+ self?.updateNetworkUI()
})
.disposed(by: self.disposeBag)
self.settingsButton.backgroundColor = nil
+ self.settingsButton.setTitle("", for: .normal)
self.settingsButton.rx.tap
.subscribe(onNext: { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
@@ -305,101 +244,56 @@
.disposed(by: self.disposeBag)
self.viewModel.currentAccountChanged
.observe(on: MainScheduler.instance)
- .subscribe(onNext: { [weak self] currentAccount in
- if let account = currentAccount {
- let accountSip = account.type == AccountType.sip
- self?.dialpadButtonShadow.isHidden = !accountSip
- self?.updateSearchBar()
- }
+ .subscribe(onNext: { [weak self] _ in
+ self?.searchBarNotActive()
})
.disposed(by: disposeBag)
- // create accounts button
+ // create account button
let accountButton = UIButton(type: .custom)
self.viewModel.profileImage.bind(to: accountButton.rx.image(for: .normal))
.disposed(by: disposeBag)
accountButton.roundedCorners = true
accountButton.clipsToBounds = true
accountButton.contentMode = .scaleAspectFill
- accountButton.cornerRadius = size * 0.5
- accountButton.frame = CGRect(x: 0, y: 0, width: size, height: size)
- accountButton.imageEdgeInsets = UIEdgeInsets(top: -4, left: -4, bottom: -4, right: -4)
- let screenRect = UIScreen.main.bounds
- let screenWidth = screenRect.size.width
- let window = UIApplication.shared.keyWindow
- let leftPadding: CGFloat = window?.safeAreaInsets.left ?? 0
- let navControllerMargin = self.navigationController?.systemMinimumLayoutMargins.leading ?? 20
- let maxWidth: CGFloat = screenWidth - 32 - navControllerMargin * 3 - leftPadding * 2
- let accountNameX: CGFloat = accountButton.frame.origin.x + accountButton.frame.size.width + margin
- let triangleViewX: CGFloat = maxWidth - triangleViewSize
- let triangleViewY: CGFloat = size * 0.5
- let accountNameWidth: CGFloat = maxWidth - triangleViewSize - size - margin * 2
- let accountName = UILabel(frame: CGRect(x: accountNameX, y: 5, width: accountNameWidth, height: size))
- accountName.tag = nameLabelTag
- let triangleView = UIView(frame: CGRect(x: triangleViewX, y: triangleViewY, width: triangleViewSize, height: triangleViewSize))
- triangleView.tag = triangleTag
- let heightWidth = triangleView.frame.size.width
- let path = CGMutablePath()
-
- path.move(to: CGPoint(x: 0, y: 0))
- path.addLine(to: CGPoint(x: heightWidth / 2, y: heightWidth / 2))
- path.addLine(to: CGPoint(x: heightWidth, y: 0))
- path.addLine(to: CGPoint(x: 0, y: 0))
-
- let shape = CAShapeLayer()
- shape.path = path
- shape.fillColor = UIColor.jamiTextBlue.cgColor
- triangleView.layer.insertSublayer(shape, at: 0)
- accountName.textAlignment = .left
- accountName.font = UIFont.systemFont(ofSize: 18.0)
- accountName.lineBreakMode = .byTruncatingTail
- accountName.textColor = UIColor.jamiTextBlue
- let openButton = UIButton(type: .custom)
- openButton.frame = CGRect(x: 0, y: 0, width: maxWidth, height: size)
- openButton.tag = openAccountTag
- self.viewModel.accountName
- .observe(on: MainScheduler.instance)
- .subscribe(onNext: { name in
- accountName.text = name
- accountName.sizeToFit()
- var frame = accountName.frame
- frame.size.width = min(frame.size.width, maxWidth - 70)
- accountName.frame = frame
- })
- .disposed(by: self.disposeBag)
- accountView = UIView(frame: CGRect(x: 0, y: 0, width: maxWidth, height: size))
- accountView.addSubview(accountButton)
- accountView.addSubview(accountName)
- accountView.addSubview(triangleView)
- accountView.addSubview(openButton)
- let accountButtonItem = UIBarButtonItem(customView: accountView)
+ accountButton.cornerRadius = smartListAccountSize * 0.5
+ accountButton.frame = CGRect(x: 0, y: 0, width: smartListAccountSize, height: smartListAccountSize)
+ accountButton.imageEdgeInsets = UIEdgeInsets(top: -smartListAccountMargin, left: -smartListAccountMargin, bottom: -smartListAccountMargin, right: -smartListAccountMargin)
+ let accountButtonItem = UIBarButtonItem(customView: accountButton)
accountButtonItem
.customView?
.translatesAutoresizingMaskIntoConstraints = false
accountButtonItem.customView?
.heightAnchor
- .constraint(equalToConstant: size).isActive = true
- accountWidth = accountView
+ .constraint(equalToConstant: smartListAccountSize).isActive = true
+ accountButtonItem.customView?
.widthAnchor
- .constraint(equalToConstant: maxWidth)
- accountWidth.isActive = true
- openButton.rx.tap
+ .constraint(equalToConstant: smartListAccountSize).isActive = true
+ accountButton.rx.tap
.throttle(Durations.halfSecond.toTimeInterval(), scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] in
self?.openAccountsList()
})
.disposed(by: self.disposeBag)
self.navigationItem.leftBarButtonItem = accountButtonItem
- self.navigationItem.rightBarButtonItem = createMenuButton()
-
- dialpadButton.rx.tap
- .subscribe(onNext: { [weak self] in
- self?.viewModel.showDialpad()
- })
- .disposed(by: self.disposeBag)
+ self.navigationItem.rightBarButtonItems = [createSearchButton(), createMenuButton()]
self.conversationsTableView.tableFooterView = UIView()
}
- func createMenuButton() -> UIBarButtonItem {
+ private func createSearchButton() -> UIBarButtonItem {
+ let imageSettings = UIImage(systemName: "square.and.pencil") as UIImage?
+ let generalSettingsButton = UIButton(type: UIButton.ButtonType.system) as UIButton
+ generalSettingsButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
+ generalSettingsButton.setImage(imageSettings, for: .normal)
+ generalSettingsButton.tintColor = .jamiMain
+ generalSettingsButton.rx.tap
+ .subscribe(onNext: { [weak self] in
+ self?.searchController.isActive = true
+ })
+ .disposed(by: self.disposeBag)
+ return UIBarButtonItem(customView: generalSettingsButton)
+ }
+
+ private func createMenuButton() -> UIBarButtonItem {
let imageSettings = UIImage(systemName: "ellipsis.circle") as UIImage?
let generalSettingsButton = UIButton(type: UIButton.ButtonType.system) as UIButton
generalSettingsButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
@@ -410,7 +304,7 @@
return UIBarButtonItem(customView: generalSettingsButton)
}
- func createMenu() -> UIMenu {
+ private func createMenu() -> UIMenu {
let configuration = UIImage.SymbolConfiguration(pointSize: 40, weight: .regular, scale: .large)
let accountImage = UIImage(systemName: "person.circle", withConfiguration: configuration)
let tintedAccountImage = accountImage?.withTintColor(.jamiMain, renderingMode: .alwaysOriginal)
@@ -435,42 +329,6 @@
return UIMenu(title: "", children: [openAccount, openSettings, aboutJami])
}
- private func updateAccountItemSize() {
- let screenRect = UIScreen.main.bounds
- let screenWidth = screenRect.size.width
- let window = UIApplication.shared.keyWindow
- let leftPadding: CGFloat = window?.safeAreaInsets.left ?? 0
- let navControllerMargin = self.navigationController?.systemMinimumLayoutMargins.leading ?? 20
- let maxWidth: CGFloat = screenWidth - 32 - navControllerMargin * 3 - leftPadding * 2
- accountWidth.constant = maxWidth
- var accountFrame = accountView.frame
- accountFrame.size.width = maxWidth
- accountView.frame = accountFrame
- if let triangle = accountView.subviews.filter({ view in
- return view.tag == triangleTag
- }).first {
- var triangleFrame = triangle.frame
- let triangleViewX: CGFloat = maxWidth - triangleViewSize - 2
- triangleFrame.origin.x = triangleViewX
- triangle.frame = triangleFrame
- }
- if let name = accountView.subviews.filter({ view in
- return view.tag == nameLabelTag
- }).first {
- var nameFrame = name.frame
- let accountNameWidth: CGFloat = maxWidth - triangleViewSize - size - margin * 2
- nameFrame.size.width = accountNameWidth
- name.frame = nameFrame
- }
- if let button = accountView.subviews.filter({ view in
- return view.tag == openAccountTag
- }).first {
- var buttonFrame = button.frame
- buttonFrame.size.width = maxWidth
- button.frame = buttonFrame
- }
- }
-
func confugureAccountPicker() {
accountPickerTextView.inputView = accounPicker
view.addSubview(accountPickerTextView)
@@ -483,7 +341,6 @@
if let account = self.viewModel.currentAccount,
let row = accountsAdapter.rowForAccountId(account: account) {
accounPicker.selectRow(row, inComponent: 0, animated: true)
- dialpadButtonShadow.isHidden = account.type == AccountType.ring
}
self.viewModel.currentAccountChanged
.observe(on: MainScheduler.instance)
@@ -599,72 +456,28 @@
.disposed(by: disposeBag)
self.conversationsTableView.rx.setDelegate(self).disposed(by: disposeBag)
+
+ // table header
+ setupTableViewHeader(for: self.conversationsTableView)
}
- let searchController: CustomSearchController = {
- let searchController = CustomSearchController(searchResultsController: nil)
- searchController.searchBar.searchBarStyle = .minimal
+ let searchController: UISearchController = {
+ let searchController = UISearchController(searchResultsController: nil)
searchController.obscuresBackgroundDuringPresentation = false
searchController.definesPresentationContext = true
searchController.hidesNavigationBarDuringPresentation = true
return searchController
}()
- func updateSearchBar() {
- guard let account = self.viewModel.currentAccount else { return }
- let accountSip = account.type == AccountType.sip
- let image = accountSip ? UIImage(asset: Asset.phoneBook) : UIImage(asset: Asset.qrCode)
- guard let buttonImage = image else { return }
- searchController.updateSearchBar(image: buttonImage)
- if !accountSip {
- let image1 = UIImage(asset: Asset.createSwarm)
- guard let buttonImage = image1 else { return }
- searchController.updateSearchBar(image: buttonImage)
- }
- }
- override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
- // -100 from total width as number of buttons in navigation items are 2
- if let container = self.searchController.searchBar.superview {
- container.frame = CGRect(x: container.frame.origin.x, y: container.frame.origin.y, width: self.view.frame.size.width - 100, height: container.frame.size.height)
- }
- }
-
func setupSearchBar() {
- guard let account = self.viewModel.currentAccount else { return }
- let accountSip = account.type == AccountType.sip
- let image = accountSip ? UIImage(asset: Asset.phoneBook) : UIImage(asset: Asset.qrCode)
- guard let buttonImage = image else { return }
- searchController
- .configureSearchBar(image: buttonImage, position: 1,
- buttonPressed: { [weak self] in
- guard let self = self else { return }
- guard let account = self.viewModel.currentAccount else { return }
- let accountSip = account.type == AccountType.sip
- if accountSip {
- self.contactPicker.delegate = self
- self.present(self.contactPicker, animated: true, completion: nil)
- } else {
- self.openScan()
- }
- })
- if !accountSip {
- let image1 = UIImage(asset: Asset.createSwarm)
- guard let buttonImage1 = image1 else { return }
- searchController
- .configureSearchBar(image: buttonImage1, position: 2,
- buttonPressed: { [weak self] in
- guard let self = self else { return }
- // guard let account = self.viewModel.currentAccount else { return }
- // let accountSip = account.type == AccountType.sip
- // if accountSip {
- // self.contactPicker.delegate = self
- // self.present(self.contactPicker, animated: true, completion: nil)
- // } else {
- self.createGroup()
- // }
- })
- }
+ searchController.delegate = self
+ let navBar = SmartListNavigationBar()
+ self.navigationController?.setValue(navBar, forKey: "navigationBar")
+
navigationItem.searchController = searchController
+ if #available(iOS 16.0, *) {
+ navigationItem.preferredSearchBarPlacement = .stacked
+ }
navigationItem.hidesSearchBarWhenScrolling = false
searchView.searchBar = searchController.searchBar
@@ -675,6 +488,115 @@
.disposed(by: disposeBag)
}
+ func willPresentSearchController(_ searchController: UISearchController) {
+ self.searchBarActive()
+ }
+
+ func updateSearchBarIfActive() {
+ if searchController.isActive {
+ searchBarActive()
+ }
+ }
+
+ func updateNetworkUI() {
+ let isHidden = self.viewModel.networkConnectionState() == .none ? false : true
+ self.networkAlertView.isHidden = isHidden
+ self.networkAlertView.isUserInteractionEnabled = !isHidden
+ if let superview = self.networkAlertView.superview {
+ superview.isUserInteractionEnabled = !isHidden
+ }
+ self.tableTopConstraint.constant = isHidden ? -60 : 15
+ self.view.layoutIfNeeded()
+ }
+
+ func searchBarNotActive() {
+ guard let customNavBar = self.navigationController?.navigationBar as? SmartListNavigationBar else { return }
+ self.navigationItem.title = selectedSegmentIndex.value == 0 ?
+ L10n.Smartlist.conversations : L10n.Smartlist.invitations
+ self.widgetsTopConstraint.constant = 0
+ updateNetworkUI()
+ customNavBar.customHeight = 44
+ customNavBar.searchActive = false
+ customNavBar.removeTopView()
+ }
+
+ func searchBarActive() {
+ guard let customNavBar = navigationController?.navigationBar as? SmartListNavigationBar else { return }
+
+ setupCommonUI(customNavBar: customNavBar)
+
+ if viewModel.isSipAccount() {
+ setupUIForSipAccount(customNavBar: customNavBar)
+ } else {
+ setupUIForNonSipAccount(customNavBar: customNavBar)
+ }
+ }
+
+ private func setupCommonUI(customNavBar: SmartListNavigationBar) {
+ navigationItem.title = ""
+ tableTopConstraint.constant = 15
+ widgetsTopConstraint.constant = 40
+ customNavBar.customHeight = 70
+ customNavBar.searchActive = true
+ }
+
+ private func setupUIForSipAccount(customNavBar: SmartListNavigationBar) {
+ let bookButton = createSearchBarButtonWithImage(named: "book.circle", weight: .light, width: 27)
+ bookButton.setImage(UIImage(asset: Asset.phoneBook), for: .normal)
+ bookButton.rx.tap
+ .subscribe(onNext: { [weak self] in
+ self?.presentContactPicker()
+ })
+ .disposed(by: customNavBar.disposeBag)
+
+ let dialpadCodeButton = createSearchBarButtonWithImage(named: "square.grid.3x3.topleft.filled", weight: .light, width: 25)
+ dialpadCodeButton.rx.tap
+ .subscribe(onNext: { [weak self] in
+ self?.viewModel.showDialpad()
+ })
+ .disposed(by: customNavBar.disposeBag)
+
+ customNavBar.addTopView(with: [bookButton, dialpadCodeButton])
+ }
+
+ private func setupUIForNonSipAccount(customNavBar: SmartListNavigationBar) {
+ let qrCodeButton = createSearchBarButtonWithImage(named: "qrcode", weight: .regular, width: 25)
+ qrCodeButton.rx.tap
+ .subscribe(onNext: { [weak self] in
+ self?.viewModel.showQRCode()
+ })
+ .disposed(by: customNavBar.disposeBag)
+
+ let swarmButton = createSearchBarButtonWithImage(named: "person.2", weight: .regular, width: 32)
+ swarmButton.rx.tap
+ .subscribe(onNext: { [weak self] in
+ self?.viewModel.createGroup()
+ })
+ .disposed(by: customNavBar.disposeBag)
+
+ customNavBar.addTopView(with: [qrCodeButton, swarmButton])
+ }
+
+ private func presentContactPicker() {
+ contactPicker.delegate = self
+ present(contactPicker, animated: true, completion: nil)
+ }
+
+ private func createSearchBarButtonWithImage(named imageName: String, weight: UIImage.SymbolWeight, width: CGFloat) -> UIButton {
+ let button = UIButton()
+ let configuration = UIImage.SymbolConfiguration(pointSize: 40, weight: weight, scale: .large)
+ button.setImage(UIImage(systemName: imageName, withConfiguration: configuration), for: .normal)
+ button.tintColor = .jamiMain
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.widthAnchor.constraint(equalToConstant: width).isActive = true
+ button.heightAnchor.constraint(equalToConstant: 23).isActive = true
+ return button
+ }
+
+ func willDismissSearchController(_ searchController: UISearchController) {
+ searchBarNotActive()
+ }
+
func startAccountCreation() {
accountPickerTextView.resignFirstResponder()
self.viewModel.createAccount()
diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
index 99e32e6..0e206b7 100644
--- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
+++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017-2019 Savoir-faire Linux Inc.
+ * Copyright (C) 2017-2023 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
@@ -25,6 +25,9 @@
import RxCocoa
import SwiftyBeaver
+let smartListAccountSize: CGFloat = 28
+let smartListAccountMargin: CGFloat = 4
+
class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource {
private let log = SwiftyBeaver.self
@@ -106,20 +109,14 @@
})
return profileImageForCurrentAccount.share()
.map({ profile in
+ let size = smartListAccountSize - (smartListAccountMargin * 3)
if let photo = profile.photo,
let data = NSData(base64Encoded: photo,
- options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
- guard let image = UIImage(data: data) else {
- return UIImage(asset: Asset.icContactPicture)!
- }
+ options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data?,
+ let image = UIImage(data: data) {
return image
}
- guard let account = self?.accountsService.currentAccount else {
- return UIImage(asset: Asset.icContactPicture)!
- }
- guard let name = profile.alias else { return UIImage.defaultJamiAvatarFor(profileName: nil, account: account) }
- let profileName = name.isEmpty ? nil : name
- return UIImage.defaultJamiAvatarFor(profileName: profileName, account: account)
+ return UIImage.defaultJamiAvatarFor(profileName: profile.alias, account: self?.accountsService.currentAccount, size: size)
})
.startWith(UIImage(asset: Asset.icContactPicture)!)
}()
@@ -196,7 +193,7 @@
}
return self.requestsService.requests
.asObservable()
- .map({ [weak self]requests -> Int in
+ .map({ [weak self] requests -> Int in
guard let self = self,
let account = self.accountsService.currentAccount else {
return 0
@@ -211,18 +208,23 @@
typealias BageValues = (messages: Int, requests: Int)
- lazy var updateSegmentControl: Observable<BageValues> = {[weak self] in
- guard let self = self else {
- let value: BageValues = (0, 0)
- return Observable.just(value)
+ lazy var updateSegmentControl: ReplaySubject<BageValues> = {
+ let subject = ReplaySubject<BageValues>.create(bufferSize: 1)
+
+ Observable.combineLatest(self.unreadMessages, self.unhandeledRequests) { (messages, requests) -> BageValues in
+ return (messages, requests)
}
- return Observable<BageValues>
- .combineLatest(self.unreadMessages,
- self.unhandeledRequests,
- resultSelector: {(messages, requests) -> BageValues in
- return (messages, requests)
- })
- .observe(on: MainScheduler.instance)
+ .observe(on: MainScheduler.instance)
+ .subscribe(onNext: { values in
+ subject.onNext(values)
+ }, onError: { error in
+ subject.onError(error)
+ }, onCompleted: {
+ subject.onCompleted()
+ })
+ .disposed(by: self.disposeBag)
+
+ return subject
}()
func reloadDataFor(accountId: String) {
@@ -344,6 +346,11 @@
}
}
+ func isSipAccount() -> Bool {
+ guard let account = self.currentAccount else { return false }
+ return account.type == .sip
+ }
+
func showSipConversation(withNumber number: String) {
guard let account = self.accountsService
.currentAccount else {
diff --git a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
index c67dca8..8a74b41 100644
--- a/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
+++ b/Ring/Ring/Features/Conversations/views/JamiSearchView/JamiSearchView.swift
@@ -108,6 +108,7 @@
.subscribe(onNext: { [weak self] status in
guard let self = self else { return }
self.searchingLabel.text = status.toString()
+ self.searchingLabel.isHidden = status.toString().isEmpty
})
.disposed(by: disposeBag)
searchingLabel.textColor = UIColor.jamiLabelColor
@@ -127,47 +128,7 @@
.debounce(Durations.textFieldThrottlingDuration.toTimeInterval(), scheduler: MainScheduler.instance)
.bind(to: self.viewModel.searchBarText)
.disposed(by: disposeBag)
-
- // Show Cancel button
- self.searchBar.rx.textDidBeginEditing
- .subscribe(onNext: { [weak self] in
- self?.editSearch.onNext(true)
- self?.searchBar.setShowsCancelButton(true, animated: false)
- })
- .disposed(by: disposeBag)
-
- // Hide Cancel button
- self.searchBar.rx.textDidEndEditing
- .subscribe(onNext: { [weak self] in
- guard let self = self else { return }
- self.searchBar.setShowsCancelButton(false, animated: false)
- if self.isIncognito && !(self.searchBar.text?.isEmpty ?? true) {
- return
- }
- self.editSearch.onNext(false)
- })
- .disposed(by: disposeBag)
-
- // Cancel button event
- self.searchBar.rx.cancelButtonClicked
- .subscribe(onNext: { [weak self] in
- self?.cancelSearch()
- })
- .disposed(by: disposeBag)
-
- // Search button event
- self.searchBar.rx.searchButtonClicked
- .subscribe(onNext: { [weak self] in
- self?.searchBar.resignFirstResponder()
- })
- .disposed(by: disposeBag)
-
- searchBar.returnKeyType = .done
- searchBar.autocapitalizationType = .none
- searchBar.tintColor = UIColor.jamiMain
searchBar.placeholder = L10n.Smartlist.searchBar
- searchBar.backgroundImage = UIImage()
- searchBar.backgroundColor = UIColor.clear
}
}
diff --git a/Ring/Ring/Features/Me/Me/BlockListViewController.swift b/Ring/Ring/Features/Me/Me/BlockListViewController.swift
index 6f9eab2..aed6002 100644
--- a/Ring/Ring/Features/Me/Me/BlockListViewController.swift
+++ b/Ring/Ring/Features/Me/Me/BlockListViewController.swift
@@ -38,7 +38,7 @@
noBlockedContactLabel.backgroundColor = UIColor.jamiBackgroundColor
noBlockedContactLabel.textColor = UIColor.jamiLabelColor
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.navigationItem.title = L10n.AccountPage.blockedContacts
}
diff --git a/Ring/Ring/Features/Me/Me/MeViewController.swift b/Ring/Ring/Features/Me/Me/MeViewController.swift
index 22d5236..1cff85e 100644
--- a/Ring/Ring/Features/Me/Me/MeViewController.swift
+++ b/Ring/Ring/Features/Me/Me/MeViewController.swift
@@ -55,7 +55,7 @@
super.viewDidLoad()
self.applyL10n()
self.configureBindings()
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.calculateSipCredentialsMargin()
self.calculateConnectivityMargin()
self.adaptTableToKeyboardState(for: self.settingsTable,
@@ -77,7 +77,7 @@
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
+ self.navigationController?.navigationBar.layer.shadowColor = UIColor.clear.cgColor
self.navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .medium),
NSAttributedString.Key.foregroundColor: UIColor.jamiLabelColor]
diff --git a/Ring/Ring/GeneralSettings/Diagnostic/LogViewController.swift b/Ring/Ring/GeneralSettings/Diagnostic/LogViewController.swift
index 0f2365d..542184b 100644
--- a/Ring/Ring/GeneralSettings/Diagnostic/LogViewController.swift
+++ b/Ring/Ring/GeneralSettings/Diagnostic/LogViewController.swift
@@ -31,7 +31,7 @@
override func viewDidLoad() {
super.viewDidLoad()
- self.configureRingNavigationBar()
+ self.configureNavigationBar()
self.navigationItem.title = L10n.LogView.title
self.navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .medium),
diff --git a/Ring/Ring/GeneralSettings/GeneralSettingsViewController.swift b/Ring/Ring/GeneralSettings/GeneralSettingsViewController.swift
index 3af37c1..bfb704f 100644
--- a/Ring/Ring/GeneralSettings/GeneralSettingsViewController.swift
+++ b/Ring/Ring/GeneralSettings/GeneralSettingsViewController.swift
@@ -42,7 +42,7 @@
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- self.navigationController?.navigationBar.layer.shadowColor = UIColor.jamiNavigationBarShadow.cgColor
+ self.navigationController?.navigationBar.layer.shadowColor = UIColor.clear.cgColor
self.navigationController?.navigationBar
.titleTextAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18, weight: .medium),
NSAttributedString.Key.foregroundColor: UIColor.jamiLabelColor]
diff --git a/Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json b/Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json
new file mode 100644
index 0000000..7cc755c
--- /dev/null
+++ b/Ring/Ring/Resources/Colors.xcassets/jamiMain.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "153",
+ "green" : "86",
+ "red" : "0"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "153",
+ "green" : "86",
+ "red" : "0"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings
index 31b49a7..831c30a 100644
--- a/Ring/Ring/Resources/en.lproj/Localizable.strings
+++ b/Ring/Ring/Resources/en.lproj/Localizable.strings
@@ -84,7 +84,7 @@
"smartlist.noResults" = "No results";
"smartlist.noConversation" = "No conversations";
"smartlist.searchBarPlaceholder" = "Enter name...";
-"smartlist.searchBar" = "Search for new or existing contact...";
+"smartlist.searchBar" = "Search";
"smartlist.noNetworkConnectivity" = "No network connectivity";
"smartlist.cellularAccess" = "Be sure cellular access is granted in your settings";
"smartlist.accountsTitle" = "Accounts";
diff --git a/Ring/Ring/SwarmInfo/SwarmInfoViewController.swift b/Ring/Ring/SwarmInfo/SwarmInfoViewController.swift
index 61f058c..644b437 100644
--- a/Ring/Ring/SwarmInfo/SwarmInfoViewController.swift
+++ b/Ring/Ring/SwarmInfo/SwarmInfoViewController.swift
@@ -31,6 +31,7 @@
override func viewDidLoad() {
super.viewDidLoad()
+ self.configureNavigationBar(isTransparent: true)
guard let swarmInfo = self.viewModel.swarmInfo else { return }
let swiftUIVM = SwarmInfoVM(with: self.viewModel.injectionBag, swarmInfo: swarmInfo)
contentView = UIHostingController(rootView: SwarmInfoView(viewmodel: swiftUIVM))
diff --git a/Ring/Ring/UI/SmartListNavigationBar.swift b/Ring/Ring/UI/SmartListNavigationBar.swift
new file mode 100644
index 0000000..e1eafe7
--- /dev/null
+++ b/Ring/Ring/UI/SmartListNavigationBar.swift
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 Savoir-faire Linux Inc.
+ *
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+import UIKit
+import RxSwift
+
+class SmartListNavigationBar: UINavigationBar {
+
+ private struct Constants {
+ static let topViewHeight: CGFloat = 50.0
+ static let buttonSpacing: CGFloat = -15.0
+ }
+
+ var topView: UIView?
+ var disposeBag = DisposeBag()
+ var usingCustomSize = false
+ var customHeight: CGFloat = 44.0
+
+ func addTopView(with buttons: [UIButton]) {
+ setupTopView()
+ setupSearchTitleLabel()
+ layoutButtons(buttons)
+ }
+
+ func removeTopView() {
+ topView?.removeFromSuperview()
+ topView = nil
+ disposeBag = DisposeBag()
+ }
+
+ var searchActive = false {
+ didSet {
+ setNeedsLayout()
+ }
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ if !usingCustomSize {
+ return
+ }
+ adjustSubviewsFrame()
+ }
+
+ override var frame: CGRect {
+ didSet {
+ if frame.size.height < customHeight && searchActive && usingCustomSize {
+ super.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: customHeight)
+ } else {
+ super.frame = frame
+ }
+ }
+ }
+}
+
+// MARK: - Private helpers
+
+private extension SmartListNavigationBar {
+
+ func setupTopView() {
+ let view = UIView()
+ view.translatesAutoresizingMaskIntoConstraints = false
+ view.backgroundColor = .clear
+
+ addSubview(view)
+
+ let guide = self.safeAreaLayoutGuide
+ NSLayoutConstraint.activate([
+ view.topAnchor.constraint(equalTo: guide.topAnchor),
+ view.heightAnchor.constraint(equalToConstant: Constants.topViewHeight),
+ view.leadingAnchor.constraint(equalTo: guide.leadingAnchor),
+ view.trailingAnchor.constraint(equalTo: guide.trailingAnchor)
+ ])
+
+ topView = view
+ }
+
+ func setupSearchTitleLabel() {
+ guard let topView = topView else { return }
+
+ let title = UILabel()
+ title.text = L10n.Smartlist.searchBar
+ title.font = UIFont.systemFont(ofSize: 18, weight: .medium)
+ title.translatesAutoresizingMaskIntoConstraints = false
+
+ topView.addSubview(title)
+ NSLayoutConstraint.activate([
+ title.centerXAnchor.constraint(equalTo: topView.centerXAnchor),
+ title.centerYAnchor.constraint(equalTo: topView.centerYAnchor)
+ ])
+ }
+
+ func layoutButtons(_ buttons: [UIButton]) {
+ guard let topView = topView else { return }
+
+ var previousButton: UIButton?
+ for button in buttons {
+ button.translatesAutoresizingMaskIntoConstraints = false
+ topView.addSubview(button)
+
+ if let prevBtn = previousButton {
+ button.trailingAnchor.constraint(equalTo: prevBtn.leadingAnchor, constant: Constants.buttonSpacing).isActive = true
+ } else {
+ button.trailingAnchor.constraint(equalTo: topView.trailingAnchor, constant: Constants.buttonSpacing).isActive = true
+ }
+ button.centerYAnchor.constraint(equalTo: topView.centerYAnchor).isActive = true
+ previousButton = button
+ }
+ }
+
+ func adjustSubviewsFrame() {
+ for subview in subviews {
+ let stringFromClass = NSStringFromClass(subview.classForCoder)
+
+ if stringFromClass.contains("UINavigationBarContentView") && searchActive {
+ subview.frame = CGRect(x: 0, y: 0, width: frame.width, height: customHeight)
+ }
+
+ if stringFromClass.contains("SearchBar") && searchActive {
+ subview.frame = CGRect(x: 0, y: 40, width: frame.width, height: subview.frame.height)
+ }
+ }
+ }
+}