123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // 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 <https://www.gnu.org/licenses/>.
- 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<String, String>
- 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
- }
- }
|