11import UIKit
22import AsyncImageKit
3+ import DesignSystem
34import WordPressShared
45import WordPressUI
56
@@ -145,6 +146,7 @@ public final class ReaderPostHeaderView: UIView {
145146 let label = UILabel ( )
146147 label. numberOfLines = 0
147148 label. adjustsFontForContentSizeCategory = true
149+ label. isUserInteractionEnabled = true
148150 label. isHidden = true
149151 return label
150152 } ( )
@@ -236,6 +238,9 @@ public final class ReaderPostHeaderView: UIView {
236238 private var featuredImageAspectConstraint : NSLayoutConstraint ?
237239 private var avatarSizeConstraints : [ NSLayoutConstraint ] = [ ]
238240 private var displaySettings : ReaderDisplaySettings = . standard
241+ private var fullExcerptText : String ?
242+ private var isExcerptExpanded = false
243+ private var lastExcerptLayoutWidth : CGFloat = 0
239244
240245 public var isShowingSubscribeLoadingIndicator : Bool = false {
241246 didSet {
@@ -331,6 +336,9 @@ public final class ReaderPostHeaderView: UIView {
331336 separator. backgroundColor = colors. border
332337
333338 subscribeLoadingIndicator. color = colors. secondaryForeground
339+
340+ lastExcerptLayoutWidth = 0
341+ updateExcerptTruncation ( )
334342 }
335343
336344 // MARK: - Private
@@ -360,6 +368,8 @@ public final class ReaderPostHeaderView: UIView {
360368 authorRow. isUserInteractionEnabled = true
361369 authorRow. addGestureRecognizer ( UITapGestureRecognizer ( target: self , action: #selector( authorTapped) ) )
362370
371+ excerptLabel. addGestureRecognizer ( UITapGestureRecognizer ( target: self , action: #selector( excerptTapped) ) )
372+
363373 featuredImageView. isUserInteractionEnabled = true
364374 featuredImageView. addGestureRecognizer ( UITapGestureRecognizer ( target: self , action: #selector( featuredImageTapped) ) )
365375
@@ -368,6 +378,16 @@ public final class ReaderPostHeaderView: UIView {
368378 apply ( . standard)
369379 }
370380
381+ public override func layoutSubviews( ) {
382+ super. layoutSubviews ( )
383+
384+ let width = mainStack. bounds. width
385+ if width > 0 && width != lastExcerptLayoutWidth {
386+ lastExcerptLayoutWidth = width
387+ updateExcerptTruncation ( )
388+ }
389+ }
390+
371391 // Extends tap area of the controls.
372392 public override func hitTest( _ point: CGPoint , with event: UIEvent ? ) -> UIView ? {
373393 let expandedViews : [ UIView ] = [ buttonSubscribe, siteNameLabel, authorRow, featuredImageView, buttonViewOriginal]
@@ -392,6 +412,14 @@ public final class ReaderPostHeaderView: UIView {
392412 delegate? . readerPostHeaderView ( self , didTap: . author)
393413 }
394414
415+ @objc private func excerptTapped( ) {
416+ guard !isExcerptExpanded, let text = fullExcerptText else { return }
417+ isExcerptExpanded = true
418+ let font = displaySettings. font ( with: . callout)
419+ let textColor = displaySettings. color. secondaryForeground
420+ excerptLabel. attributedText = NSAttributedString ( string: text, attributes: [ . font: font, . foregroundColor: textColor] )
421+ }
422+
395423 @objc private func featuredImageTapped( ) {
396424 delegate? . readerPostHeaderView ( self , didTap: . featuredImage)
397425 }
@@ -445,13 +473,73 @@ public final class ReaderPostHeaderView: UIView {
445473
446474 private func configureExcerpt( with excerpt: String ? ) {
447475 if let excerpt, !excerpt. isEmpty {
448- excerptLabel . text = excerpt
476+ fullExcerptText = excerpt
449477 excerptLabel. isHidden = false
478+ lastExcerptLayoutWidth = 0
479+ updateExcerptTruncation ( )
450480 } else {
481+ fullExcerptText = nil
451482 excerptLabel. isHidden = true
452483 }
453484 }
454485
486+ private func updateExcerptTruncation( ) {
487+ guard let text = fullExcerptText, !text. isEmpty, !isExcerptExpanded else { return }
488+
489+ let font = displaySettings. font ( with: . callout)
490+ let textColor = displaySettings. color. secondaryForeground
491+ let atttributes : [ NSAttributedString . Key : Any ] = [ . font: font, . foregroundColor: textColor]
492+ let availableWidth = mainStack. bounds. width
493+
494+ guard availableWidth > 0 else {
495+ excerptLabel. attributedText = NSAttributedString ( string: text, attributes: atttributes)
496+ return
497+ }
498+
499+ let maxHeight = font. lineHeight * CGFloat( Constants . excerptMaxLines) + 1
500+
501+ func isEnoughSpace( for string: String , maxHeight: CGFloat ) -> Bool {
502+ let height = ( string as NSString ) . boundingRect (
503+ with: CGSize ( width: availableWidth, height: . greatestFiniteMagnitude) ,
504+ options: [ . usesLineFragmentOrigin, . usesFontLeading] ,
505+ attributes: atttributes,
506+ context: nil
507+ ) . height
508+ return height <= maxHeight
509+ }
510+
511+ // Hide under the cut only if there is enough text to warrant it. If there is only one extra
512+ // line, there is no reason to cut it.
513+ if isEnoughSpace ( for: text, maxHeight: maxHeight + font. leading * 1 ) {
514+ excerptLabel. attributedText = NSAttributedString ( string: text, attributes: atttributes)
515+ return
516+ }
517+
518+ let suffix = " " + Strings. viewMore
519+
520+ // Find the longest prefix that fits with the suffix.
521+ var low = 0 , high = text. count, bestCut = 0
522+ while low <= high {
523+ let mid = ( low + high) / 2
524+ if isEnoughSpace ( for: String ( text. prefix ( mid) ) + suffix, maxHeight: maxHeight) {
525+ bestCut = mid
526+ low = mid + 1
527+ } else {
528+ high = mid - 1
529+ }
530+ }
531+
532+ let trimmed = String ( text. prefix ( bestCut) ) . trimmingCharacters ( in: . whitespacesAndNewlines)
533+ let result = NSMutableAttributedString ( string: trimmed, attributes: atttributes)
534+ result. append (
535+ NSAttributedString ( string: suffix, attributes: [
536+ . font: font. withWeight ( . regular) ,
537+ . foregroundColor: UIColor . label,
538+ ] )
539+ )
540+ excerptLabel. attributedText = result
541+ }
542+
455543 private func configureReadingTime( with readingTime: String ) {
456544 readingTimeLabel. text = readingTime
457545 }
@@ -464,6 +552,7 @@ private extension ReaderPostHeaderView {
464552 static let padding : CGFloat = 16
465553 static let avatarSize : CGFloat = 32
466554 static let avatarSizeRegular : CGFloat = 40
555+ static let excerptMaxLines : Int = 5
467556 static let defaultFeaturedImageAspectRatio : CGFloat = 9.0 / 16.0
468557 static let maxFeaturedImageAspectRatio : CGFloat = 2.0
469558 }
@@ -480,6 +569,11 @@ private enum Strings {
480569 value: " View Original " ,
481570 comment: " Button in the reader post header to view the original post in a browser "
482571 )
572+ static let viewMore = AppLocalizedString (
573+ " reader.post.header.viewMore " ,
574+ value: " \u{2026} view more " ,
575+ comment: " Appended to the truncated excerpt in the reader post header to indicate more content is available "
576+ )
483577}
484578
485579// MARK: - Preview
0 commit comments