123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // 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 public protocol OEMentionsHelperDelegate: class {
- func mentionSelected(id:Int, name:String)
- func textView(_ growingTextView: HPGrowingTextView!, willChangeHeight height: Float)
- func textView(_ growingTextView: HPGrowingTextView!, shouldChangeTextIn range: NSRange, replacementText text: String!) -> Bool
- func textViewDidChange(_ growingTextView: HPGrowingTextView!)
- }
- @objc public class OEMentionsHelper: NSObject {
-
- @objc open weak var delegate: OEMentionsHelperDelegate?
- private var oementions: OEMentions
- private var growingTextView: HPGrowingTextView
- private var topLine: UIView
- private var mainView: UIView
-
- private let regex: String = "@\\[[0-9A-Z*@]{8}\\]"
- private var mentionCountBeforeChange: Int = 0
- private var shouldUpdateTextColor: Bool = false
- private var isDictationRunning: Bool = false
-
- @objc required public init(containerView: UIView, chatInputView: HPGrowingTextView, mainView: UIView, sortedContacts: [Contact]) {
- let memberlist = OEMentionsHelper.buildOeObjectsList(sortedContacts: sortedContacts)
- growingTextView = chatInputView
- self.mainView = mainView
- oementions = OEMentions.init(containerView: containerView, textView: chatInputView.internalTextView, mainView: mainView, oeObjects: memberlist)
- topLine = UIView.init()
- topLine.frame.size.height = 1
- topLine.backgroundColor = Colors.hairline()
- topLine.isHidden = true
- mainView.insertSubview(topLine, aboveSubview: oementions.tableView)
- super.init()
- oementions.delegate = self
- chatInputView.delegate = self
- oementions.nameFont = chatInputView.internalTextView.font!
- setupColors()
- oementions.showMentionFullInContainer = false
- oementions.tableView.addObserver(self, forKeyPath: "hidden", options: [.new], context: nil)
- }
-
- @objc public func setupColors() {
- oementions.nameColor = Colors.fontLight()
- oementions.notMentionColor = growingTextView.internalTextView.textColor!
- oementions.changeMentionTableviewBackground(color: Colors.background())
- oementions.changeMentionTableviewSeparatorColor(color: Colors.hairline())
- updateTextColor()
- }
-
- @objc public func formattedMentionText() -> String {
- var textViewText = growingTextView.internalTextView.text
- var difference = 0
- if oementions.mentionsIndexes.count != 0 {
- let mentionsIndexes = oementions.mentionsIndexes.sorted(by: { $0.0 < $1.0 })
- for (index,dict) in mentionsIndexes {
- let length = dict["length"] as! Int
-
- let key = dict["key"] as! String
- let nsRange = NSMakeRange(index + difference, length)
- let range = Range.init(nsRange, in: textViewText!)
- textViewText?.replaceSubrange(range!, with: key)
-
-
- let nameCount = length
- let replaceCount = key.count
- difference = difference + (replaceCount - nameCount)
- }
- }
- return textViewText!
- }
-
- @objc public func addMentions(draft: String) {
- var draftString = draft
- do {
- let mentionRegex = try NSRegularExpression.init(pattern: regex, options: .caseInsensitive)
-
- var finished = false
- var lastNotFoundIndex = -1
-
- while !finished {
- let mentionResult = mentionRegex.matches(in: draftString, options: .reportCompletion, range: NSRange.init(location: 0, length: draftString.utf16.count))
-
- var result: NSTextCheckingResult? = nil
- if lastNotFoundIndex == -1 {
- result = mentionResult.first
- } else {
- if mentionResult.count >= lastNotFoundIndex + 2 {
- result = mentionResult[lastNotFoundIndex + 1]
- }
- }
-
- if result == nil {
- finished = true
- break
- }
-
- let mentionTag = String(draftString[String.Index(utf16Offset: result!.range.location, in: draftString)...String.Index(utf16Offset: result!.range.location + result!.range.length - 1, in: draftString)])
- if mentionTag.count == 11 {
- let identity = String(mentionTag[String.Index(utf16Offset: 2, in: mentionTag)...String.Index(utf16Offset: 9, in: mentionTag)]).uppercased()
- let contact = ContactStore.shared().contact(forIdentity: identity)
-
- if contact != nil || identity == MyIdentityStore.shared()?.identity || identity == "@@@@@@@@" {
- var displayName = BundleUtil.localizedString(forKey: "me")
-
- if let nickname = MyIdentityStore.shared().pushFromName {
- if nickname.utf16.count > 0 {
- displayName = nickname
- }
- }
-
- if contact != nil {
- displayName = contact!.mentionName
- }
- else if identity == "@@@@@@@@" {
- displayName = BundleUtil.localizedString(forKey: "mentions_all")
- }
-
- let range = Range.init(result!.range, in: draftString)
- draftString = draftString.replacingCharacters(in: range!, with: "@\(displayName!)")
-
- let dict = ["key": mentionTag, "length": displayName!.utf16.count + 1] as [String : Any]
- oementions.mentionsIndexes[result!.range.location] = dict
- } else {
- let range = Range.init(result!.range, in: draftString)
- draftString = draftString.replacingCharacters(in: range!, with: "@\(identity)")
- if lastNotFoundIndex == -1 {
- lastNotFoundIndex = 0
- } else {
- lastNotFoundIndex += 1
- }
- }
- } else {
- if lastNotFoundIndex == -1 {
- lastNotFoundIndex = 0
- } else {
- lastNotFoundIndex += 1
- }
- }
- }
- growingTextView.text = draftString
- updateTextColor()
- }
- catch {
- print("failed regex draft for mentions")
- }
- }
-
-
-
- @objc public func resetMentionsIndexes() {
- oementions.mentionsIndexes.removeAll()
- }
-
- @objc public func updateContainterViewFrame() {
- // add space on top of input view
- oementions.textViewHeight = growingTextView.frame.size.height + 3.0
- oementions.updatePosition()
- topLine.frame = CGRect.init(x: oementions.tableView.frame.origin.x, y: oementions.tableView.frame.origin.y - 1, width: oementions.tableView.frame.size.width, height: 1)
- }
- @objc public func updateTextColor() {
- if !isDictationRunning {
- var attributes = [NSAttributedString.Key: AnyObject]()
- attributes[.foregroundColor] = oementions.notMentionColor
- attributes[.font] = oementions.nameFont
- if oementions.mentionsIndexes.count != 0 {
- let attributedString: NSMutableAttributedString = NSMutableAttributedString.init(string: oementions.textView!.text, attributes: attributes)
-
- let mentionsIndexes = oementions.mentionsIndexes.sorted(by: { $0.0 < $1.0 })
- for (index,dict) in mentionsIndexes {
- let length = dict["length"] as! Int
- attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: oementions.nameColor, range: NSMakeRange(index, length))
- attributedString.addAttribute(NSAttributedString.Key.font, value: oementions.nameFont, range: NSMakeRange(index, length))
- attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: Colors.fontLink()!, range: NSMakeRange(index, length))
- }
-
- if let selectedRange = oementions.textView!.selectedTextRange {
- oementions.textView!.attributedText = attributedString
- // and only if the new position is valid
- if let newPosition = oementions.textView!.position(from: selectedRange.start, in: UITextLayoutDirection.left, offset: 0) {
- // set the new position
- oementions.textView!.selectedTextRange = oementions.textView!.textRange(from: newPosition, to: newPosition)
- }
- } else {
- oementions.textView!.attributedText = attributedString
- }
- } else {
- if let selectedRange = oementions.textView!.selectedTextRange {
- oementions.textView!.attributedText = NSMutableAttributedString.init(string: oementions.textView!.text, attributes: attributes)
- // and only if the new position is valid
- if let newPosition = oementions.textView!.position(from: selectedRange.start, in: UITextLayoutDirection.left, offset: 0) {
- // set the new position
- oementions.textView!.selectedTextRange = oementions.textView!.textRange(from: newPosition, to: newPosition)
- }
- } else {
- oementions.textView!.attributedText = NSMutableAttributedString.init(string: oementions.textView!.text, attributes: attributes)
- }
- }
- }
- }
-
- @objc public func updateOeObjects(sortedContacts: [Contact]) {
- oementions.setOeObjects(oeObjects: OEMentionsHelper.buildOeObjectsList(sortedContacts: sortedContacts))
- }
-
- class func buildOeObjectsList(sortedContacts: [Contact]) -> [OEObject] {
- var memberlist = [OEObject]()
- // add @all contact
- let oeObject = OEObject.init(id: 0, name: "@" + BundleUtil.localizedString(forKey: "all"), key: "@[@@@@@@@@]", object: nil)
- memberlist.append(oeObject)
-
- var i = 1
- for contact in sortedContacts {
- let oeObject = OEObject.init(id: i, name: "@" + contact.displayName, key: "@[\(contact.identity!)]", object: contact)
- memberlist.append(oeObject)
- i += 1
- }
- return memberlist
- }
-
- override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- let tableView = object as! UITableView
- topLine.isHidden = tableView.isHidden
- }
-
- private func startKeyboardObserver() {
- NotificationCenter.default.addObserver(self, selector: #selector(changeInputMode), name: UITextInputMode.currentInputModeDidChangeNotification, object: nil)
- }
-
- private func stopKeyboardObserver() {
- NotificationCenter.default.removeObserver(self)
- }
-
- @objc func changeInputMode(notification: NSNotification) {
- let inputMethod = growingTextView.textInputMode?.primaryLanguage
- if inputMethod == "dictation" {
- isDictationRunning = true
- } else {
- isDictationRunning = false
- }
- }
- }
- extension OEMentionsHelper: HPGrowingTextViewDelegate {
-
- public func growingTextViewDidBeginEditing(_ growingTextView: HPGrowingTextView!) {
- startKeyboardObserver()
- }
- public func growingTextView(_ growingTextView: HPGrowingTextView!, willChangeHeight height: Float) {
- delegate?.textView(growingTextView, willChangeHeight: height)
- }
-
- public func growingTextView(_ growingTextView: HPGrowingTextView!, didChangeHeight height: Float) {
- updateContainterViewFrame()
- }
-
- public func growingTextView(_ growingTextView: HPGrowingTextView!, shouldChangeTextIn range: NSRange, replacementText text: String!) -> Bool {
- mentionCountBeforeChange = oementions.mentionsIndexes.count
- _ = oementions.textView(growingTextView.internalTextView, shouldChangeTextIn: range, replacementText: text)
- return delegate?.textView(growingTextView, shouldChangeTextIn: range, replacementText: text) ?? true
- }
-
- public func growingTextViewDidChange(_ growingTextView: HPGrowingTextView!) {
- if growingTextView.internalTextView.isFirstResponder {
- oementions.updatePosition()
- }
- if shouldUpdateTextColor == true || (mentionCountBeforeChange > oementions.mentionsIndexes.count && oementions.mentionsIndexes.count == 0) {
- shouldUpdateTextColor = false
- updateTextColor()
- }
- delegate?.textViewDidChange(growingTextView)
- }
-
- public func growingTextViewDidEndEditing(_ growingTextView: HPGrowingTextView!) {
- stopKeyboardObserver()
- oementions.textViewDidEndEditing(growingTextView.internalTextView)
- }
-
- }
- extension OEMentionsHelper: OEMentionsDelegate {
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath, oeObject: OEObject) -> UITableViewCell {
- let cell:MentionCell = MentionCell.init(style: UITableViewCell.CellStyle.default, reuseIdentifier: "MentionCell")
- cell.backgroundColor = UIColor.clear
- cell.selectionStyle = UITableViewCell.SelectionStyle.none
- if let contact = oeObject.object as? Contact {
- cell.contact = contact
- } else {
- // @all contact
- cell.allContact = true
- }
- return cell
- }
-
- func mentionSelected(id: Int, name: String) {
- delegate?.mentionSelected(id: id, name: name)
- growingTextView.refreshHeight()
- updateTextColor()
- }
-
- func tableViewPositionUpdated() {
- if !oementions.tableView.isHidden {
- topLine.frame = CGRect.init(x: oementions.tableView.frame.origin.x, y: oementions.tableView.frame.origin.y, width: oementions.tableView.frame.size.width, height: 1)
- }
- }
-
- func textViewShouldUpdateTextColor() {
- shouldUpdateTextColor = true
- }
- }
- class MentionCell : UITableViewCell {
-
- var contact : Contact? {
- didSet {
- avatar.image = AvatarMaker.shared()?.avatar(for: contact!, size: 16.0, masked: true)
- mentionNameLabel.text = contact?.displayName
- mentionIdentityLabel.text = contact?.identity
- }
- }
-
- var allContact: Bool = false {
- didSet {
- avatar.image = AvatarMaker.shared()?.unknownPersonImage()
- mentionNameLabel.text = "@" + BundleUtil.localizedString(forKey: "all")
- mentionIdentityLabel.text = nil
- }
- }
-
- private let mentionNameLabel : UILabel = {
- let lbl = UILabel()
- lbl.textColor = Colors.fontNormal()
- lbl.font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
- lbl.textAlignment = .left
- return lbl
- }()
-
-
- private let mentionIdentityLabel : UILabel = {
- let lbl = UILabel()
- lbl.textColor = Colors.fontNormal()
- lbl.font = UIFont.systemFont(ofSize: UIFont.systemFontSize)
- lbl.textAlignment = .right
- lbl.numberOfLines = 0
- lbl.lineBreakMode = .byWordWrapping
- return lbl
- }()
-
- private let avatar : UIImageView = {
- let imgView = UIImageView()
- imgView.contentMode = .scaleAspectFit
- imgView.clipsToBounds = true
- return imgView
- }()
-
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
-
- let stretchingView = UIView()
- stretchingView.setContentHuggingPriority(UILayoutPriority(rawValue: 1), for: .horizontal)
- stretchingView.backgroundColor = .clear
- stretchingView.translatesAutoresizingMaskIntoConstraints = false
-
- let stackView = UIStackView(arrangedSubviews: [avatar, mentionNameLabel,stretchingView,mentionIdentityLabel])
- stackView.distribution = .fill
- stackView.axis = .horizontal
- stackView.spacing = 15
- stackView.alignment = .fill
- addSubview(stackView)
- avatar.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 32.0, height: 32.0, enableInsets: false)
- stackView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 10, paddingLeft: 15, paddingBottom: 10, paddingRight: 15, width: 0, height: 0, enableInsets: false)
-
- }
-
- required init?(coder aDecoder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- }
- extension UIView {
-
- func anchor (top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat, enableInsets: Bool) {
- var topInset = CGFloat(0)
- var bottomInset = CGFloat(0)
-
- if #available(iOS 11, *), enableInsets {
- let insets = self.safeAreaInsets
- topInset = insets.top
- bottomInset = insets.bottom
- }
-
- translatesAutoresizingMaskIntoConstraints = false
-
- if let top = top {
- self.topAnchor.constraint(equalTo: top, constant: paddingTop+topInset).isActive = true
- }
- if let left = left {
- self.leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
- }
- if let right = right {
- rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
- }
- if let bottom = bottom {
- bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom-bottomInset).isActive = true
- }
- if height != 0 {
- heightAnchor.constraint(equalToConstant: height).isActive = true
- }
- if width != 0 {
- widthAnchor.constraint(equalToConstant: width).isActive = true
- }
-
- }
-
- }
|