mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			288 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			288 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
 | 
						|
@objc(OWSMediaAlbumView)
 | 
						|
public class MediaAlbumView: UIStackView {
 | 
						|
    private let items: [ConversationMediaAlbumItem]
 | 
						|
 | 
						|
    @objc
 | 
						|
    public let itemViews: [MediaView]
 | 
						|
 | 
						|
    @objc
 | 
						|
    public var moreItemsView: MediaView?
 | 
						|
 | 
						|
    private static let kSpacingPts: CGFloat = 2
 | 
						|
    private static let kMaxItems = 5
 | 
						|
 | 
						|
    @available(*, unavailable, message: "use other init() instead.")
 | 
						|
    required public init(coder aDecoder: NSCoder) {
 | 
						|
        notImplemented()
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public required init(mediaCache: NSCache<NSString, AnyObject>,
 | 
						|
                         items: [ConversationMediaAlbumItem],
 | 
						|
                         isOutgoing: Bool,
 | 
						|
                         maxMessageWidth: CGFloat) {
 | 
						|
        self.items = items
 | 
						|
        self.itemViews = MediaAlbumView.itemsToDisplay(forItems: items).map {
 | 
						|
            let result = MediaView(mediaCache: mediaCache,
 | 
						|
                                  attachment: $0.attachment,
 | 
						|
                                  isOutgoing: isOutgoing,
 | 
						|
                                  maxMessageWidth: maxMessageWidth)
 | 
						|
            return result
 | 
						|
        }
 | 
						|
 | 
						|
        super.init(frame: .zero)
 | 
						|
 | 
						|
        // UIStackView's backgroundColor property has no effect.
 | 
						|
        addBackgroundView(withBackgroundColor: Colors.navigationBarBackground)
 | 
						|
 | 
						|
        createContents(maxMessageWidth: maxMessageWidth)
 | 
						|
    }
 | 
						|
 | 
						|
    private func createContents(maxMessageWidth: CGFloat) {
 | 
						|
        switch itemViews.count {
 | 
						|
        case 0:
 | 
						|
            owsFailDebug("No item views.")
 | 
						|
            return
 | 
						|
        case 1:
 | 
						|
            // X
 | 
						|
            guard let itemView = itemViews.first else {
 | 
						|
                owsFailDebug("Missing item view.")
 | 
						|
                return
 | 
						|
            }
 | 
						|
            addSubview(itemView)
 | 
						|
            itemView.autoPinEdgesToSuperviewEdges()
 | 
						|
        case 2:
 | 
						|
            // X X
 | 
						|
            // side-by-side.
 | 
						|
            let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
 | 
						|
            autoSet(viewSize: imageSize, ofViews: itemViews)
 | 
						|
            for itemView in itemViews {
 | 
						|
                addArrangedSubview(itemView)
 | 
						|
            }
 | 
						|
            self.axis = .horizontal
 | 
						|
            self.spacing = MediaAlbumView.kSpacingPts
 | 
						|
        case 3:
 | 
						|
            //   x
 | 
						|
            // X x
 | 
						|
            // Big on left, 2 small on right.
 | 
						|
            let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3
 | 
						|
            let bigImageSize = smallImageSize * 2 + MediaAlbumView.kSpacingPts
 | 
						|
 | 
						|
            guard let leftItemView = itemViews.first else {
 | 
						|
                owsFailDebug("Missing view")
 | 
						|
                return
 | 
						|
            }
 | 
						|
            autoSet(viewSize: bigImageSize, ofViews: [leftItemView])
 | 
						|
            addArrangedSubview(leftItemView)
 | 
						|
 | 
						|
            let rightViews = Array(itemViews[1..<3])
 | 
						|
            addArrangedSubview(newRow(rowViews: rightViews,
 | 
						|
                                      axis: .vertical,
 | 
						|
                                      viewSize: smallImageSize))
 | 
						|
            self.axis = .horizontal
 | 
						|
            self.spacing = MediaAlbumView.kSpacingPts
 | 
						|
        case 4:
 | 
						|
            // X X
 | 
						|
            // X X
 | 
						|
            // Square
 | 
						|
            let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
 | 
						|
 | 
						|
            let topViews = Array(itemViews[0..<2])
 | 
						|
            addArrangedSubview(newRow(rowViews: topViews,
 | 
						|
                                      axis: .horizontal,
 | 
						|
                                      viewSize: imageSize))
 | 
						|
 | 
						|
            let bottomViews = Array(itemViews[2..<4])
 | 
						|
            addArrangedSubview(newRow(rowViews: bottomViews,
 | 
						|
                                      axis: .horizontal,
 | 
						|
                                      viewSize: imageSize))
 | 
						|
 | 
						|
            self.axis = .vertical
 | 
						|
            self.spacing = MediaAlbumView.kSpacingPts
 | 
						|
        default:
 | 
						|
            // X X
 | 
						|
            // xxx
 | 
						|
            // 2 big on top, 3 small on bottom.
 | 
						|
            let bigImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2
 | 
						|
            let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3
 | 
						|
 | 
						|
            let topViews = Array(itemViews[0..<2])
 | 
						|
            addArrangedSubview(newRow(rowViews: topViews,
 | 
						|
                                      axis: .horizontal,
 | 
						|
                                      viewSize: bigImageSize))
 | 
						|
 | 
						|
            let bottomViews = Array(itemViews[2..<5])
 | 
						|
            addArrangedSubview(newRow(rowViews: bottomViews,
 | 
						|
                                      axis: .horizontal,
 | 
						|
                                      viewSize: smallImageSize))
 | 
						|
 | 
						|
            self.axis = .vertical
 | 
						|
            self.spacing = MediaAlbumView.kSpacingPts
 | 
						|
 | 
						|
            if items.count > MediaAlbumView.kMaxItems {
 | 
						|
                guard let lastView = bottomViews.last else {
 | 
						|
                    owsFailDebug("Missing lastView")
 | 
						|
                    return
 | 
						|
                }
 | 
						|
 | 
						|
                moreItemsView = lastView
 | 
						|
 | 
						|
                let tintView = UIView()
 | 
						|
                tintView.backgroundColor = UIColor(white: 0, alpha: 0.4)
 | 
						|
                lastView.addSubview(tintView)
 | 
						|
                tintView.autoPinEdgesToSuperviewEdges()
 | 
						|
 | 
						|
                let moreCount = max(1, items.count - MediaAlbumView.kMaxItems)
 | 
						|
                let moreCountText = OWSFormat.formatInt(Int32(moreCount))
 | 
						|
                let moreText = String(format: NSLocalizedString("MEDIA_GALLERY_MORE_ITEMS_FORMAT",
 | 
						|
                                                                comment: "Format for the 'more items' indicator for media galleries. Embeds {{the number of additional items}}."), moreCountText)
 | 
						|
                let moreLabel = UILabel()
 | 
						|
                moreLabel.text = moreText
 | 
						|
                moreLabel.textColor = UIColor.ows_white
 | 
						|
                // We don't want to use dynamic text here.
 | 
						|
                moreLabel.font = UIFont.systemFont(ofSize: 24)
 | 
						|
                lastView.addSubview(moreLabel)
 | 
						|
                moreLabel.autoCenterInSuperview()
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        for itemView in itemViews {
 | 
						|
            guard moreItemsView != itemView else {
 | 
						|
                // Don't display the caption indicator on
 | 
						|
                // the "more" item, if any.
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            guard let index = itemViews.firstIndex(of: itemView) else {
 | 
						|
                owsFailDebug("Couldn't determine index of item view.")
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            let item = items[index]
 | 
						|
            guard let caption = item.caption else {
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            guard caption.count > 0 else {
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            guard let icon = UIImage(named: "media_album_caption") else {
 | 
						|
                owsFailDebug("Couldn't load icon.")
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            let iconView = UIImageView(image: icon)
 | 
						|
            itemView.addSubview(iconView)
 | 
						|
            itemView.layoutMargins = .zero
 | 
						|
            iconView.autoPinTopToSuperviewMargin(withInset: 6)
 | 
						|
            iconView.autoPinLeadingToSuperviewMargin(withInset: 6)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private func autoSet(viewSize: CGFloat,
 | 
						|
                         ofViews views: [MediaView]) {
 | 
						|
        for itemView in views {
 | 
						|
            itemView.autoSetDimensions(to: CGSize(width: viewSize, height: viewSize))
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private func newRow(rowViews: [MediaView],
 | 
						|
                        axis: NSLayoutConstraint.Axis,
 | 
						|
                        viewSize: CGFloat) -> UIStackView {
 | 
						|
        autoSet(viewSize: viewSize, ofViews: rowViews)
 | 
						|
        return newRow(rowViews: rowViews, axis: axis)
 | 
						|
    }
 | 
						|
 | 
						|
    private func newRow(rowViews: [MediaView],
 | 
						|
                        axis: NSLayoutConstraint.Axis) -> UIStackView {
 | 
						|
        let stackView = UIStackView(arrangedSubviews: rowViews)
 | 
						|
        stackView.axis = axis
 | 
						|
        stackView.spacing = MediaAlbumView.kSpacingPts
 | 
						|
        return stackView
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func loadMedia() {
 | 
						|
        for itemView in itemViews {
 | 
						|
            itemView.loadMedia()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func unloadMedia() {
 | 
						|
        for itemView in itemViews {
 | 
						|
            itemView.unloadMedia()
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private class func itemsToDisplay(forItems items: [ConversationMediaAlbumItem]) -> [ConversationMediaAlbumItem] {
 | 
						|
        // TODO: Unless design changes, we want to display
 | 
						|
        //       items which are still downloading and invalid
 | 
						|
        //       items.
 | 
						|
        let validItems = items
 | 
						|
        guard validItems.count < kMaxItems else {
 | 
						|
            return Array(validItems[0..<kMaxItems])
 | 
						|
        }
 | 
						|
        return validItems
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public class func layoutSize(forMaxMessageWidth maxMessageWidth: CGFloat,
 | 
						|
                                 items: [ConversationMediaAlbumItem]) -> CGSize {
 | 
						|
        let itemCount = itemsToDisplay(forItems: items).count
 | 
						|
        switch itemCount {
 | 
						|
        case 0, 1, 4:
 | 
						|
            // X
 | 
						|
            //
 | 
						|
            // or
 | 
						|
            //
 | 
						|
            // XX
 | 
						|
            // XX
 | 
						|
            // Square
 | 
						|
            return CGSize(width: maxMessageWidth, height: maxMessageWidth)
 | 
						|
        case 2:
 | 
						|
            // X X
 | 
						|
            // side-by-side.
 | 
						|
            let imageSize = (maxMessageWidth - kSpacingPts) / 2
 | 
						|
            return CGSize(width: maxMessageWidth, height: imageSize)
 | 
						|
        case 3:
 | 
						|
            //   x
 | 
						|
            // X x
 | 
						|
            // Big on left, 2 small on right.
 | 
						|
            let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
 | 
						|
            let bigImageSize = smallImageSize * 2 + kSpacingPts
 | 
						|
            return CGSize(width: maxMessageWidth, height: bigImageSize)
 | 
						|
        default:
 | 
						|
            // X X
 | 
						|
            // xxx
 | 
						|
            // 2 big on top, 3 small on bottom.
 | 
						|
            let bigImageSize = (maxMessageWidth - kSpacingPts) / 2
 | 
						|
            let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3
 | 
						|
            return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func mediaView(forLocation location: CGPoint) -> MediaView? {
 | 
						|
        var bestMediaView: MediaView?
 | 
						|
        var bestDistance: CGFloat = 0
 | 
						|
        for itemView in itemViews {
 | 
						|
            let itemCenter = convert(itemView.center, from: itemView.superview)
 | 
						|
            let distance = CGPointDistance(location, itemCenter)
 | 
						|
            if bestMediaView != nil && distance > bestDistance {
 | 
						|
                continue
 | 
						|
            }
 | 
						|
            bestMediaView = itemView
 | 
						|
            bestDistance = distance
 | 
						|
        }
 | 
						|
        return bestMediaView
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func isMoreItemsView(mediaView: MediaView) -> Bool {
 | 
						|
        return moreItemsView == mediaView
 | 
						|
    }
 | 
						|
}
 |