-1

I need to implement show more/ show less in UILabel like facebook. My label has mention and url features. Everything worked perfectly when there is no emoji in the text. I have added trailing at the end of the label by calculating textHeight and visible text of the label.

This is my extension file

//
//  extension.swift
//  SeeMore
//
//  Created by Macbook Pro on 14/10/20.
//

import Foundation
import UIKit
//MARK : -  text height, weidth
extension Range where Bound == String.Index {
    var nsRange:NSRange {
        return NSRange(location: self.lowerBound.encodedOffset,
                       length: self.upperBound.encodedOffset -
                        self.lowerBound.encodedOffset)
    }
}
extension String {
    var extractURLs: [URL] {
        var urls : [URL] = []
        var error: NSError?
        let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
        
        var text = self
        
        detector!.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { (result: NSTextCheckingResult!, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer<ObjCBool>) -> Void in
            //            println("\(result)")
            print("Extracted Result: \(result.url)")
            urls.append(result.url!)
        })
        
        return urls
    }
    func textHeight(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
        
        return boundingBox.height
    }
    
    func textWidth(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
        
        return boundingBox.width
    }
}


extension UILabel{
    
    func indexOfAttributedTextCharacterAtPoint(point: CGPoint, font : UIFont) -> Int {
        
        guard let attributedString = self.attributedText else { return -1 }
        
        let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
        // Add font so the correct range is returned for multi-line labels
        mutableAttribString.addAttributes([NSAttributedString.Key.font: font], range: NSRange(location: 0, length: attributedString.length))
        
        let textStorage = NSTextStorage(attributedString: mutableAttribString)
        
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        
        let textContainer = NSTextContainer(size: frame.size)
        textContainer.lineFragmentPadding = 0
        textContainer.maximumNumberOfLines = numberOfLines
        textContainer.lineBreakMode = lineBreakMode
        layoutManager.addTextContainer(textContainer)
        
        let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        return index
    }
    
    func addTrailingForShowLess(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor, complition: @escaping (_ attribute: NSMutableAttributedString) -> Void) {
        
        let readMoreText: String = trailingText + moreText
        
        
        if let myText = self.text {
            let trimmedString: String? = myText + trailingText
            
            let readMoreLength: Int = (readMoreText.count)
            
            guard let safeTrimmedString = trimmedString else { return }
            
            if safeTrimmedString.count <= readMoreLength { return }
            
            print("less this number \(safeTrimmedString.count) should never be less\n")
            print("less then this number \(readMoreLength)")
            
            // "safeTrimmedString.count - readMoreLength" should never be less then the readMoreLength because it'll be a negative value and will crash
            //                let trimmedForReadMore: String = (safeTrimmedString as NSString).replacingCharacters(in: NSRange(location: safeTrimmedString.count, length: readMoreLength), with: "") + trailingText
            
            let answerAttributed = NSMutableAttributedString(string: safeTrimmedString, attributes: [NSAttributedString.Key.font: moreTextFont])
            let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
            answerAttributed.append(readMoreAttributed)
            complition(answerAttributed)
            //            self.attributedText = answerAttributed
        }
    }
    func addTrailing(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor, complition: @escaping (_ attribute: NSMutableAttributedString) -> Void) {
        
        let readMoreText: String = trailingText + moreText
        if self.text?.count == 0 { return }
        if self.visibleTextLength == 0 { return }
        
        let lengthForVisibleString: Int = self.visibleTextLength
        
        if let myText = self.text {
            let mutableString: String = myText
            
            let trimmedString: String? = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: myText.count - lengthForVisibleString), with: "")
            
            let readMoreLength: Int = (readMoreText.count + 2)
            
            guard let safeTrimmedString = trimmedString else { return }
            
            if safeTrimmedString.count <= readMoreLength { return }
            
            print("this number \(safeTrimmedString.count) should never be less\n")
            print("then this number \(readMoreLength)")
            
            // "safeTrimmedString.count - readMoreLength" should never be less then the readMoreLength because it'll be a negative value and will crash
            let trimmedForReadMore: String = (safeTrimmedString as NSString).replacingCharacters(in: NSRange(location: safeTrimmedString.count - readMoreLength, length: readMoreLength), with: "") + trailingText
            
            let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSAttributedString.Key.font: moreTextFont])
            let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
            answerAttributed.append(readMoreAttributed)
            complition(answerAttributed)
            //            self.attributedText = answerAttributed
        }
    }
    
    var visibleTextLength: Int {
        
        let font: UIFont = self.font
        let mode: NSLineBreakMode = self.lineBreakMode
        let screenSize = UIScreen.main.bounds
        let labelWidth: CGFloat = self.frame.size.width
        let labelHeight: CGFloat = self.frame.size.height
        let sizeConstraint = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude)
        
        if let myText = self.text {
            
            let attributes: [AnyHashable: Any] = [NSAttributedString.Key.font: font]
            let attributedText = NSAttributedString(string: myText, attributes: attributes as? [NSAttributedString.Key : Any])
            let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
            
            if boundingRect.size.height > labelHeight {
                var index: Int = 0
                var prev: Int = 0
                
                let characterSet = CharacterSet.whitespacesAndNewlines
                repeat {
                    prev = index
                    if mode == NSLineBreakMode.byCharWrapping {
                        index += 1
                    } else {
                        index = (myText as NSString).rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: myText.count - index - 1)).location
                    }
                } while index != NSNotFound && index < myText.count && (myText as NSString).substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes as? [NSAttributedString.Key : Any], context: nil).size.height <= labelHeight
                return prev
            }
        }
        
        if self.text == nil {
            return 0
        } else {
            return self.text!.count
        }
    }
    
}

This is my tableView cell

 //
//  PostTableViewCell.swift
//  SeeMore
//
//  Created by Macbook Pro on 14/10/20.
//

import UIKit
protocol PostTableViewCellDelegate {
    func postLabelAction(cell: PostTableViewCell, post: Post, tap: UITapGestureRecognizer)
}

class PostTableViewCell: UITableViewCell {

    @IBOutlet weak var postLabel: UILabel!
    var currentPost : Post?
    var delegate: PostTableViewCellDelegate?
    override func awakeFromNib() {
        super.awakeFromNib()
        postLabel.textColor = .black
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(postLabelSelected))
        tapGesture.numberOfTapsRequired = 1
        postLabel.isUserInteractionEnabled = true
        postLabel.addGestureRecognizer(tapGesture)
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    @objc func postLabelSelected(tap: UITapGestureRecognizer) {
        delegate?.postLabelAction(cell: self, post: currentPost!, tap: tap)
    }
    
    func applyAttributedStringToPost(attributedString: NSMutableAttributedString, item: Post) {
        let text = attributedString.string
        let urls = text.extractURLs
        for url in urls {
            let range1 = (text as NSString).range(of: url.absoluteString)
            attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: range1)
            postLabel.attributedText = attributedString
        }
        postLabel.attributedText = attributedString
    }
    
    func setupItems(item : Post){
        currentPost = item
        let postText = item.postText
        postLabel.numberOfLines = item.isExpendable ? 0 : 4
        postLabel.text = postText
        let underlineAttriString = NSMutableAttributedString(string: postText)
        let urls = postText.extractURLs
        for url in urls {
            let range1 = (postText as NSString).range(of: url.absoluteString)
            underlineAttriString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: range1)
            postLabel.attributedText = underlineAttriString
        }
        //Apply See more see less
        postLabel.sizeToFit()
//        let screenSize = UIScreen.main.bounds/
        if let newfont = postLabel.font {
            let textHeight = postText.textHeight(withConstrainedWidth: postLabel.frame.size.height, font: newfont)
            if item.isExpendable {
                postLabel.addTrailingForShowLess(with: "...", moreText: "Show Less", moreTextFont: newfont, moreTextColor: UIColor.blue) { (attributedString) in
                    self.applyAttributedStringToPost(attributedString: attributedString, item: item)
                }
            } else if postLabel.frame.size.height < textHeight, !item.isExpendable {
                postLabel.addTrailing(with: "...", moreText: "Show More", moreTextFont: newfont, moreTextColor: UIColor.blue) { (attributedString) in
                    self.applyAttributedStringToPost(attributedString: attributedString, item: item)
                }
            }
        }
    }
}

This is my view controller

//  ViewController.swift
//  SeeMore
//
//  Created by Macbook Pro on 14/10/20.
//

import UIKit
struct Post {
    var postText = ""
    var isExpendable = false
}
class ViewController: UIViewController {
    var postArray : [Post] = [Post]()
    @IBOutlet weak var postTableView: UITableView!{
        didSet{
            settingUpTableView()
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        fillUpPostData()
        postTableView.reloadData()
    }
    func settingUpTableView() {
        let nib = UINib(nibName: "PostTableViewCell", bundle: nil)
        postTableView.register(nib, forCellReuseIdentifier: "PostTableViewCell")
        postTableView.rowHeight = UITableView.automaticDimension
        postTableView.estimatedRowHeight = 300
        postTableView.separatorStyle = .singleLine
        postTableView.separatorInset = .zero
    }

    func fillUpPostData() {
        postArray.append(Post(postText: "Next, we established questions. We used❓liberally to indicate questions, and  to indicate “I’m not understanding.” I’d say questions composed about 30–40% of our communication, so this was a critical emoji discovery.\nThen, we added more interesting complex relationships.  implied that, after time, one thing would lead to another. ️ means that “I’ll be able to talk soon.” We created a scale for asking “How do you feel?”: ☹️❓\nAs our communication advanced, we adopted each others’ language. My friend used  to indicate “and.” After I understood, I started adopting the same emoji meaning. Since communication was so challenging and time-intensive, cooperating on each others’ emoji meaning was critical.\nTime in emoji is very expressive.  allowed us to communicate time very easily", isExpendable: false))
        postArray.append(Post(postText: "Apple has just finished its “Hi, Speed” event, where it finally took the wraps off the four new iPhone 12 phones, which have all-new designs and will all support 5G wireless networks. Apple also unveiled the HomePod mini, a smaller and more affordable version of the HomePod smart speaker.\nIf you want to read the play-by-play of the event, check out our live blog with commentary from Dieter Bohn and Nilay Patel. But if you just want to read the biggest news from the show, we’ve got you covered right here.\nApple has just finished its “Hi, Speed” event, where it finally took the wraps off the four new iPhone 12 phones, which have all-new designs and will all support 5G wireless networks. Apple also unveiled the HomePod mini, a smaller and more affordable version of the HomePod smart speaker.\nIf you want to read the play-by-play of the event, check out our live blog with commentary from Dieter Bohn and Nilay Patel. But if you just want to read the biggest news from the show, we’ve got you covered right here....Show Less", isExpendable: false))
        postArray.append(Post(postText: "Next, we established questions. We used❓liberally to indicate questions, and  to indicate \"I’m not understanding.\" I’d say questions composed about 30–40% of our communication, so this was a critical emoji discovery.\nThen, we added more interesting complex relationships.  implied that, after time, one thing would lead to another. ️ means that \"I’ll be able to talk soon.\" We created a scale for asking “How do you feel?\": ☹️❓\nAs our communication advanced, we adopted each others’ language. My friend used  to indicate \"and.", isExpendable: false))
        postArray.append(Post(postText: "BIG breaking news! Trump is going to have a fit over this one. “A former Department of Homeland Security official questioned in a scathing op-ed how anyone could vote for President Donald Trump in the 2020 election, citing the chaos that has characterized his administration and his mishandling of the coronavirus pandemic.\nElizabeth Neumann explained in a USA Today column ❤️☦️ published Tuesday why she is “convinced” the president is “failing at keeping Americans safe.” Neumann, until April, spent three years as a high-ranking member of the Trump administration’s national security team.\n“He is dangerous for our country,” she wrote.\nNeumann cited the president’s failure to address the surge in white nationalist violence, his constant lying and the turnover of key administration officials. Without elaborating, she also referred to what she said was a close call that could have led the U.S. into war.\n\nShe warned the country’s abandonment of allies and appeasement of dictators would “only get worse” in a second Trump term. Her column is headlined: “Trump made it hard for me to protect America. How could I vote for him again? How could anyone?”\n\nNeumann, ❤️☦️ who endorsed Democratic nominee Joe Biden in a video released by the Republican Voters Against Trump group in August, had particularly harsh commentary on the pandemic.\n\nTrump’s intentional public dismissal of the threat of COVID-19 — while acknowledging its “deadly” nature in private — was worse than a “dereliction of duty,” said Neumann.\n\n“Your government is supposed to perform some basic functions; keeping you and your family safe is primary among them,” she wrote. “In 2016, I voted for President Trump. But when someone asked me if I could vote for him again, after he time and again refused to keep Americans safe — how could I say anything but no? How could anyone?” ", isExpendable: false))
        postArray.append(Post(postText: " \n\n\nHere is her entire op-Ed: Everything we saw during the first presidential debate is indicative of how President Donald Trump behaves in the White House. His business model is chaos. ❤️☦️He has no organization, no leadership, and sees every interaction as a contest or a battle, even when it doesn’t have to be. Chris Wallace now knows how so many administration staffers feel — and how I felt when the president got in the way of me doing my job. He is dangerous for our country.\n\nI served as the Assistant Secretary of Homeland Security for Counterterrorism and Threat Prevention, and my job was to help keep Americans safe from terrorist attacks. My time in office coincided with a dramatic rise in white nationalist violence, but my colleagues and I couldn’t get the president to help address the problem. At the debate, America saw what I saw in the administration: President Trump refuses to distance himself from white nationalists. ❤️☦️I realized after watching the White House response to the terrorist attack in El Paso that his rhetoric was a recruitment tool for violent extremist groups. The president bears some responsibility for the deaths of Americans at the hands of these violent extremists.\n\n\nAs a conservative, I believe a primary purpose of the federal government is to provide for the national defense. Under the Constitution, it is a mandatory function of the federal government. After serving for three years inside the Trump administration’s national security team, I am convinced the president is failing at keeping Americans safe.\n\nEarly on in the administration, I represented the Department of Homeland Security at several meetings in which a White House staff member implied that the president had approved, and that we should begin to carry out, plans that could have led the United States into war. Thankfully, there were experienced people in the room who had enough clout to suggest that these catastrophic plans needed a second look. Many of us weren’t sure what, if anything, the president had actually approved, or that he had been properly briefed to ensure he understood the risks involved. These people helped us avoid war.", isExpendable: false))
        postArray.append(Post(postText: "\n\nHaving adults in the room matters. They protect the country from a chaotic White House structure that allows staffers to run amok. But more importantly, they ensure that the president is presented with unvarnished truth, that difficult topics like domestic terrorism are raised even when he ❤️☦️doesn’t want to acknowledge them.\n\nEvery day, the number of experienced people in the administration that have the ability to speak truth to power shrinks. We are seeing the results: abandoning our allies and  cozying up to dictators, emboldening our enemies and weakening our standing in the world. This would only get worse in a second Trump term.\n\nJust as he ignored white nationalists, he also ignored COVID-19. After nearly 20 years around national crises, you can recognize a good response — and a bad one. By late January, professionals and outside experts were sounding the alarms. Health and Human Services Secretary Alex Azar declared a public health emergency on Jan. 31. But throughout February, longtime colleagues at the Federal ❤️☦️Emergency Management Agency were privately expressing dismay at the flailing response.\n\nBob Woodward’s revelations last month, using the president’s own words, demonstrate that the government’s failure wasn’t just because of bureaucracy, the newness of the virus, or even the president sticking his head in the sand. It was intentional. This is worse than dereliction of duty — this is willfully leading defenseless people to a killer. President Trump repeatedly lied to the American public. Those lies have led to more deaths and illnesses.\n\nA recent study assessed that had we exercised our pandemic mitigation plans (which have existed since 2005), as other wealthy countries did, nearly 9 million more Americans would be employed, and over 100,000 would still be alive. We could have saved half of the Americans who have died so far.\n\nYour government is supposed to perform some basic functions; keeping you and your family safe is primary among them. In 2016, I voted for President Trump. But when someone asked me if I could vote for him again, after he time and again refused to keep Americans safe — how could I say anything but no? How could anyone?...Show Less", isExpendable: false))
        postArray.append(Post(postText: "\nPamela Lindsay:\n”just another conspiracy theory that proved to be untrue look at all the money he has wasted for something that was patently untrue. how many does this make that he yelled about some half baked ❤️☦️conspiracy theory that just showed how desperate he is. yet look at the number that will still follow him he and they are truly deplorable.\nBeverly Jones: ”Tax dollars wasted in another attempt to blame a former President of wrong-doing. Since we already know #45's wrongdoings, an investigation will cost nothing.", isExpendable: false))
        postArray.append(Post(postText: "Judy Stamberger:\n \"AN incredible, 8 yrs served with dignity, kindness, no lawsuits, arrests, no impeachment, no crude remarks, respectful man & his family.\"\nJames S Lechleitnerjr:\n\"Outstanding men Martin Luther King, and President Barack Obama...\nAmen❗️❤️☦️‼️\"\nCarol Brown-Hatcher:\n\"Greatness, Grace, Integrity, Honorable(Men,) Brilliant, Humble, Brave, Courageous. THEY WOULD NOT BE SILENCED. They knew the meaning of \"Responsibility\".", isExpendable: false))
        postArray.append(Post(postText: "BRAVO!  We love this!!! A North Dakota farmer is going viral for plowing a pro-Biden-Harris message into his field. Peter Larson says that this is the first time he has used his fields to share his political views. He hopes to encourage others to get out and vote. Great idea, Peter!  Follow Ridin' With Biden to defeat Trump and save America! ...Show Less", isExpendable: false))
        postArray.append(Post(postText: "Ridin' With Biden:\n\"Olivia Troye, who until a couple of months ago was on Mike Pence's FAILED Coronavirus \"Response\" Task Force, just slammed Trump for PRETENDING that Dr. Fauci praised Trump's disastrous COVID response in a misleading campaign ad. Follow Ridin' With Biden to defeat Trump and save America!\" ", isExpendable: false))
        
    }
}
extension ViewController: UITableViewDelegate, UITableViewDataSource{
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return postArray.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "PostTableViewCell", for: indexPath) as! PostTableViewCell
        cell.setupItems(item: postArray[indexPath.row])
        cell.delegate = self
        return cell
    }
}

extension ViewController: PostTableViewCellDelegate{
    func postLabelAction(cell: PostTableViewCell,post: Post, tap: UITapGestureRecognizer) {
        let tapLocation = tap.location(in: cell.postLabel)
        let tapIndex = cell.postLabel.indexOfAttributedTextCharacterAtPoint(point: tapLocation, font: cell.postLabel.font!)
        var rangeArray: [NSRange] = [NSRange]()
        var linkRangeArray: [NSRange] = [NSRange]()
        let postText = cell.postLabel.text
        var seemoreText = ""
        seemoreText = post.isExpendable ? "Show Less" : "Show More"
        //
        if let seeRange = postText?.range(of: seemoreText)?.nsRange {
            if tapIndex > seeRange.location && tapIndex < seeRange.location + seeRange.length {
                if let indexPath = postTableView.indexPath(for: cell) {
                    let isExpanded = self.postArray[indexPath.row].isExpendable
                    self.postArray[indexPath.row].isExpendable = !isExpanded

                    DispatchQueue.main.async { [weak self] in
                        self?.postTableView.reloadRows(at: [indexPath], with: .automatic)
                        self?.postTableView.scrollToRow(at: indexPath, at: .top, animated: true)
                    }
                    return
                }
            }
        }
        if let urls = postText?.extractURLs {
            for url in urls {
                guard let range = postText?.range(of: url.absoluteString)?.nsRange else { return }
                linkRangeArray.append(range)
            }

            for (index, neRange) in linkRangeArray.enumerated() {
                if tapIndex > neRange.location && tapIndex < neRange.location + neRange.length {
                    print("link print")
                    let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LinkPreviewWebViewController") as! LinkPreviewWebViewController
                    vc.loadURL = urls[index].absoluteString

                    self.navigationController?.isNavigationBarHidden = false
                    self.navigationController?.pushViewController(vc, animated: true)
                    return
                }
            }
        }
        // mention logic will be there
        
        
    }
}

If there are emoji's or extra line gap before post text visibleTextLength return wrong count as a result users can't see the show more text because it is below visible text. even label.frame.size doesn't give me the current width of the different devices. Any help will be greatly appreciated. TIA

[Please ignore the post text as it's taken from real user to produce the problem] It should be mentioned that i have tried Expendable Label and ReadMoreTextView but they can't serve me well.

This the output with the problem i'm facing

  • Can you include simple code demonstrating the problem? Have you looked at [TTTAttributedLabel](https://github.com/TTTAttributedLabel/TTTAttributedLabel)? Someone posted [here](https://stackoverflow.com/a/56936597/6257435) and it looks pretty much like what you're asking about. – DonMag Oct 14 '20 at 13:01
  • I don't want to use any library as most of the time they conflict with some requirements. – Kamrul Hassan Sabuj Oct 14 '20 at 13:07
  • I'm not a fan of 3rd-party libs either... but you could dig into it to try and find some help for your code. Can you edit your question with a simple example? Much easier to offer help if we don't have to spend time figuring out how to setup and reproduce your issue. – DonMag Oct 14 '20 at 13:13
  • Thanks for your response. I have edited my post, please have a look.TIA. You have to create a xib file to work on it. – Kamrul Hassan Sabuj Oct 14 '20 at 16:31

1 Answers1

-1

You can use https://github.com/apploft/ExpandableLabel this Class for show more/read more/more functionality in UILabel. This class is Customizable as per your requirements. I use this it will working fine.

I hope it will work for you. Thanks.

Virani Vivek
  • 888
  • 1
  • 8
  • 22
  • I have used it before, sometimes it doesn't work. more importantly , i couldn't add single tap gesture(which is must needed on my case for going to mentioned user profile and link url) on it. it only allowed me to use long press gesture. – Kamrul Hassan Sabuj Oct 14 '20 at 12:29