// _____ _
// |_ _| |_ _ _ ___ ___ _ __ __ _
// | | | ' \| '_/ -_) -_) ' \/ _` |_
// |_| |_||_|_| \___\___|_|_|_\__,_(_)
//
// Threema iOS Client
// Copyright (c) 2019-2020 Threema GmbH
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License, version 3,
// as published by the Free Software Foundation.
//
// 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
import Foundation
@objc class CompanyDirectoryViewController: ThemedViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchBar: SearchBarWithoutCancelButton!
@IBOutlet weak var noEntriesFoundView: UIView!
@IBOutlet weak var noEntriesFoundTitleLabel: UILabel!
@IBOutlet weak var noEntriesFoundDescriptionLabel: UILabel!
@IBOutlet weak var activeFiltersView: UIStackView!
@IBOutlet weak var scrollView: UIScrollView!
@objc var addContactActive: Bool = true
var filterArray: [String] = [String]()
private var contactsWithSections: [[CompanyDirectoryContact]] = [[CompanyDirectoryContact]]()
private var sectionTitles: [String] = [String]()
private var allSectionTitles: [String] = [String]()
private var nextPage: Int = 0
private var showLoadMore: Bool = false
private var searchString: String = ""
private var resultArray: [CompanyDirectoryContact] = [CompanyDirectoryContact]()
private let collation = UILocalizedIndexedCollation.current()
override func viewDidLoad() {
super.viewDidLoad()
searchBar.sizeToFit()
searchBar.placeholder = BundleUtil.localizedString(forKey: "companydirectory_placeholder")
tableView.setupAutoAdjust()
noEntriesFoundTitleLabel.text = BundleUtil.localizedString(forKey: "companydirectory_noentries_title")
noEntriesFoundDescriptionLabel.text = BundleUtil.localizedString(forKey: "companydirectory_noentries_description")
self.title = MyIdentityStore.shared()?.companyName
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if addContactActive == false {
navigationItem.leftBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .cancel, target: self, action: #selector(cancel))
}
navigationItem.rightBarButtonItem = UIBarButtonItem.init(image: BundleUtil.imageNamed("Filter"), style: .plain, target: self, action: #selector(filter))
self.searchBar.becomeFirstResponder()
performSearch()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowFilterSegue" {
if let destinationVC = segue.destination as? CompanyDirectoryCategoryViewController {
destinationVC.companyDirectoryViewController = self
}
}
}
override func refresh() {
super.refresh()
setupColors()
updateNoEntriesFound()
setupFiltersView()
tableView.reloadData()
}
func setupColors() {
Colors.update(searchBar)
Colors.update(tableView)
noEntriesFoundTitleLabel.textColor = Colors.fontNormal()
noEntriesFoundDescriptionLabel.textColor = Colors.fontLight()
}
func updateNoEntriesFound() {
if contactsWithSections.count > 0 {
tableView.tableFooterView = nil
} else {
tableView.tableFooterView = noEntriesFoundView
showLoadMore = false
}
}
@objc private func performSearch() {
guard let query = searchBar.text, query.trimmingCharacters(in: .whitespaces) != "" else {
contactsWithSections.removeAll()
sectionTitles.removeAll()
resultArray.removeAll()
refresh()
return
}
searchBar.isLoading = true
// call api to get the results
contactsWithSections.removeAll()
sectionTitles.removeAll()
resultArray.removeAll()
nextPage = 0
searchString = query
ServerAPIConnector().search(inDirectory: query, categories: filterArray, page: Int32(nextPage), for: LicenseStore.shared(), for: MyIdentityStore.shared(), onCompletion: { (contacts, paging) in
self.showLoadMore = false
if contacts != nil {
if contacts!.count > 0 {
for dict in contacts! {
let contact = CompanyDirectoryContact.init(dictionary: dict as! [AnyHashable : Any?])
self.resultArray.append(contact)
}
if UserSettings.shared().sortOrderFirstName == true {
let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.first))
self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
self.sectionTitles = arrayTitles
self.allSectionTitles = allSectionTitles
} else {
let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.last))
self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
self.sectionTitles = arrayTitles
self.allSectionTitles = allSectionTitles
}
if let next = paging?["next"] as? Int {
if let total = paging?["total"] as? Int {
if total != self.resultArray.count {
self.nextPage = next
self.showLoadMore = true
}
}
}
}
}
self.refresh()
self.searchBar.isLoading = false
}) { (error) in
if let theError = (error as NSError?) {
if theError.code == 100 {
UIAlertTemplate.showAlert(owner: self, title: BundleUtil.localizedString(forKey: "cannot_connect_title"), message: BundleUtil.localizedString(forKey: "cannot_connect_message"))
}
}
self.refresh()
self.searchBar.isLoading = false
}
}
private func loadMore(cell: UITableViewCell) {
let activityView = UIActivityIndicatorView.init(style: .gray)
switch Colors.getTheme() {
case ColorThemeDark, ColorThemeDarkWork:
activityView.style = .white
break
case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
activityView.style = .gray
break
default:
activityView.style = .gray
break
}
activityView.startAnimating()
cell.accessoryView = activityView
ServerAPIConnector().search(inDirectory: searchString, categories: filterArray, page: Int32(nextPage), for: LicenseStore.shared(), for: MyIdentityStore.shared(), onCompletion: { (contacts, paging) in
self.showLoadMore = false
if contacts != nil {
if contacts!.count > 0 {
for dict in contacts! {
let contact = CompanyDirectoryContact.init(dictionary: dict as! [AnyHashable : Any?])
self.resultArray.append(contact)
}
if UserSettings.shared().sortOrderFirstName == true {
let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.first))
self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
self.sectionTitles = arrayTitles
self.allSectionTitles = allSectionTitles
} else {
let (arrayContacts, arrayTitles, allSectionTitles) = self.collation.partitionObjects(array: self.resultArray, collationStringSelector: #selector(getter: CompanyDirectoryContact.last))
self.contactsWithSections = arrayContacts as! [[CompanyDirectoryContact]]
self.sectionTitles = arrayTitles
self.allSectionTitles = allSectionTitles
}
if let next = paging?["next"] as? Int {
if let total = paging?["total"] as? Int {
if total != self.resultArray.count {
self.nextPage = next
self.showLoadMore = true
}
}
}
}
}
activityView.stopAnimating()
cell.accessoryView = nil
self.refresh()
self.searchBar.isLoading = false
}) { (error) in
activityView.stopAnimating()
cell.accessoryView = nil
self.searchBar.isLoading = false
}
}
private func setupFiltersView() {
if filterArray.count > 0 {
self.scrollView.frame = CGRect.init(x: self.scrollView.frame.origin.x, y: self.scrollView.frame.origin.y, width: self.scrollView.frame.size.width, height: 44.0)
self.scrollView.setNeedsLayout()
self.scrollView.layoutIfNeeded()
} else {
self.scrollView.frame = CGRect.init(x: self.scrollView.frame.origin.x, y: self.scrollView.frame.origin.y, width: self.scrollView.frame.size.width, height: 0)
self.scrollView.setNeedsLayout()
self.scrollView.layoutIfNeeded()
}
for tempView in activeFiltersView.subviews {
tempView.removeFromSuperview()
}
activeFiltersView.layoutIfNeeded()
var i = 0
let catDict = MyIdentityStore.shared().directoryCategories as! Dictionary
for category in filterArray {
let filterLabel = createFilterLabel(text: catDict[category]!, index: i)
activeFiltersView.addArrangedSubview(filterLabel)
i += 1
}
}
private func createFilterLabel(text: String, index: Int) -> UIStackView {
let textWidth = text.widthOfString(usingFont: UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote))
let buttonWidth: CGFloat = 23.0
let padding: CGFloat = 2.0
let stackView = UIStackView.init(frame: CGRect.init(x: 0.0, y: 0.0, width: textWidth + buttonWidth + (padding * 2), height: activeFiltersView.frame.size.height))
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.spacing = 0
let textlabel = UILabel.init(frame: CGRect.init(x: 0.0, y: 0.0, width: textWidth, height: 20.0))
textlabel.textColor = Colors.fontInverted()
textlabel.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.footnote)
textlabel.text = text
stackView.addArrangedSubview(textlabel)
let button = UIButton(type: .custom)
button.frame = CGRect(x: 0.0, y: 0.0, width: buttonWidth, height: buttonWidth)
button.backgroundColor = .clear
button.layer.cornerRadius = CGFloat(button.frame.size.width)/CGFloat(2.0)
button.setImage(UIImage(named: "CloseCategory", in: .white), for: .normal)
button.tag = index
button.imageView?.contentMode = .scaleAspectFit
button.addTarget(self, action: #selector(removeTag(_:)), for: .touchUpInside)
stackView.addArrangedSubview(button)
let widthContraints = NSLayoutConstraint(item: button, attribute: NSLayoutConstraint.Attribute.width, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: buttonWidth)
NSLayoutConstraint.activate([widthContraints])
stackView.layoutIfNeeded()
let size = CGSize(width: textWidth, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let attributes = [NSAttributedString.Key.font: textlabel.font]
let rectangleHeight = String(text).boundingRect(with: size, options: options, attributes: attributes as [NSAttributedString.Key : Any], context: nil).height
let backgroundFrame = CGRect.init(x: stackView.frame.origin.x - (padding * 3), y: ((stackView.frame.size.height - rectangleHeight) / 2) - padding, width: stackView.frame.size.width + (padding * 2), height: rectangleHeight + (padding * 2))
let backgroundView = UIView.init(frame: backgroundFrame)
backgroundView.layer.cornerRadius = 5
backgroundView.backgroundColor = Colors.backgroundInverted()
stackView.insertSubview(backgroundView, at: 0)
return stackView
}
@objc private func cancel() {
self.dismiss(animated: true, completion: nil)
}
@objc private func filter() {
self.performSegue(withIdentifier: "ShowFilterSegue", sender: self)
}
@objc private func removeTag(_ sender: AnyObject) {
filterArray.remove(at: (sender.tag))
performSearch()
}
}
extension CompanyDirectoryViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
if showLoadMore {
return sectionTitles.count + 1
}
return sectionTitles.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if showLoadMore && section == sectionTitles.count {
return 1
}
return contactsWithSections[section].count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if showLoadMore && indexPath.section == sectionTitles.count {
return 50.0
}
return UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if showLoadMore && indexPath.section == sectionTitles.count {
let cell:UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "LoadMoreCell", for: indexPath)
cell.textLabel?.text = BundleUtil.localizedString(forKey: "loadMore")
let image = UIImage.init(named: "ArrowDown", in: Colors.fontLight())
cell.imageView?.image = image?.resizedImage(newSize: CGSize.init(width: 25.0, height: 25.0))
cell.accessoryView = nil
return cell
} else {
let cell:CompanyDirectoryContactCell = tableView.dequeueReusableCell(withIdentifier: "CompanyDirectoryContactCell", for: indexPath) as! CompanyDirectoryContactCell
let contact = contactsWithSections[indexPath.section][indexPath.row]
cell.addContactActive = addContactActive
cell.contact = contact
return cell
}
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
Colors.update(cell)
if cell.isKind(of: CompanyDirectoryContactCell.self) {
(cell as! CompanyDirectoryContactCell).setupColors()
}
}
func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return UILocalizedIndexedCollation.current().sectionIndexTitles
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if showLoadMore && section == sectionTitles.count {
return ""
}
return sectionTitles[section]
}
func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
if title == "*" {
return sectionTitles.count
}
if sectionTitles.contains(title) == true {
return sectionTitles.firstIndex(of: title) ?? 0
} else {
var tempIndex:Int = 0
for str in allSectionTitles {
if sectionTitles.contains(str) == true {
tempIndex += 1
}
if str == title {
return tempIndex - 1
}
}
return 0
}
}
}
extension CompanyDirectoryViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if nextPage > 0 && indexPath.section == sectionTitles.count {
let cell = tableView.cellForRow(at: indexPath)
let activityIndicator = UIActivityIndicatorView.init(style: .white)
switch Colors.getTheme() {
case ColorThemeDark, ColorThemeDarkWork:
activityIndicator.style = .white
break
case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
activityIndicator.style = .gray
break
default:
activityIndicator.style = .gray
break
}
activityIndicator.startAnimating()
cell?.accessoryView = activityIndicator
loadMore(cell: cell!)
} else {
let directoryContact = contactsWithSections[indexPath.section][indexPath.row]
let contact = ContactStore.shared()?.addWorkContact(withIdentity: directoryContact.id, publicKey: directoryContact.pk, firstname: directoryContact.first, lastname: directoryContact.last)
// show chat
if contact != nil {
navigationController?.dismiss(animated: true, completion: {
let info = [kKeyContact: contact!, kKeyForceCompose: true] as [String : Any]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: kNotificationShowConversation), object: nil, userInfo: info)
})
}
}
}
}
extension CompanyDirectoryViewController: UISearchBarDelegate {
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
//here you should call the function which will update your data source and reload table view (or other UI that you have)
performSearch()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
if searchText.count >= 3 {
perform(#selector(performSearch), with: nil, afterDelay: 0.75)
} else {
contactsWithSections.removeAll()
sectionTitles.removeAll()
resultArray.removeAll()
refresh()
}
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
}
}
class SearchBarWithoutCancelButton:UISearchBar {
override func layoutSubviews() {
super.layoutSubviews()
self.setShowsCancelButton(false, animated: false)
}
public var textField: UITextField? {
let subViews = subviews.flatMap { $0.subviews }
guard let textField = (subViews.filter { $0 is UITextField }).first as? UITextField else {
return nil
}
return textField
}
public var activityIndicator: UIActivityIndicatorView? {
return textField?.leftView?.subviews.compactMap{ $0 as? UIActivityIndicatorView }.first
}
var isLoading: Bool {
get {
return activityIndicator != nil
} set {
if newValue {
if activityIndicator == nil {
let newActivityIndicator: UIActivityIndicatorView
switch Colors.getTheme() {
case ColorThemeDark, ColorThemeDarkWork:
newActivityIndicator = UIActivityIndicatorView(style: .white)
newActivityIndicator.backgroundColor = UIColor.init(red: 32.0/255.0, green: 32.0/255.0, blue: 29.0/255.0, alpha: 1.0)
break
case ColorThemeUndefined, ColorThemeLight, ColorThemeLightWork:
newActivityIndicator = UIActivityIndicatorView(style: .gray)
newActivityIndicator.backgroundColor = .white
break
default:
newActivityIndicator = UIActivityIndicatorView(style: .gray)
newActivityIndicator.backgroundColor = .white
break
}
newActivityIndicator.startAnimating()
textField?.leftView?.addSubview(newActivityIndicator)
let leftViewSize = textField?.leftView?.frame.size ?? CGSize.zero
newActivityIndicator.center = CGPoint(x: leftViewSize.width/2, y: leftViewSize.height/2)
}
} else {
activityIndicator?.removeFromSuperview()
}
}
}
}
extension UITableView {
func setupAutoAdjust() {
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardshown), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardhide), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc func keyboardshown(_ notification:Notification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
self.fitContentInset(inset: UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0))
}
}
@objc func keyboardhide(_ notification:Notification) {
if ((notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue) != nil {
self.fitContentInset(inset: .zero)
}
}
func fitContentInset(inset:UIEdgeInsets!) {
self.contentInset = inset
self.scrollIndicatorInsets = inset
}
}
extension UILocalizedIndexedCollation {
//func for partition array in sections
func partitionObjects(array:[AnyObject], collationStringSelector:Selector) -> ([AnyObject], [String], [String]) {
var unsortedSections = [[AnyObject]]()
//1. Create a array to hold the data for each section
for _ in self.sectionTitles {
unsortedSections.append([]) //appending an empty array
}
//2. Put each objects into a section
for item in array {
let index:Int = self.section(for: item, collationStringSelector:collationStringSelector)
unsortedSections[index].append(item)
}
//3. sorting the array of each section
var activeSectionTitles = [String]()
var sections = [AnyObject]()
for index in 0 ..< unsortedSections.count { if unsortedSections[index].count > 0 {
activeSectionTitles.append(self.sectionTitles[index])
sections.append(self.sortedArray(from: unsortedSections[index], collationStringSelector: collationStringSelector) as AnyObject)
}
}
return (sections, activeSectionTitles, sectionTitles)
}
}
extension String {
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSAttributedString.Key.font: font]
let size = self.size(withAttributes: fontAttributes)
return size.width
}
}