diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2e4c3def24..56406aed02 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -274,6 +274,7 @@ 34C2EEB02270B8E200BCA1D0 /* StickerKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C2EEAF2270B8E100BCA1D0 /* StickerKeyboard.swift */; }; 34C2EEB22270CC8E00BCA1D0 /* StickerPackCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C2EEB12270CC8D00BCA1D0 /* StickerPackCollectionView.swift */; }; 34C2EEB42270D1CE00BCA1D0 /* StickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C2EEB32270D1CE00BCA1D0 /* StickerView.swift */; }; + 34C2EEB62270FF7C00BCA1D0 /* LinearHorizontalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C2EEB52270FF7B00BCA1D0 /* LinearHorizontalLayout.swift */; }; 34C3C78D20409F320000134C /* Opening.m4r in Resources */ = {isa = PBXBuildFile; fileRef = 34C3C78C20409F320000134C /* Opening.m4r */; }; 34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 34C3C78E2040A4F70000134C /* sonarping.mp3 */; }; 34C3C7922040B0DD0000134C /* OWSAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -985,6 +986,7 @@ 34C2EEAF2270B8E100BCA1D0 /* StickerKeyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerKeyboard.swift; sourceTree = ""; }; 34C2EEB12270CC8D00BCA1D0 /* StickerPackCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackCollectionView.swift; sourceTree = ""; }; 34C2EEB32270D1CE00BCA1D0 /* StickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerView.swift; sourceTree = ""; }; + 34C2EEB52270FF7B00BCA1D0 /* LinearHorizontalLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearHorizontalLayout.swift; sourceTree = ""; }; 34C3C78C20409F320000134C /* Opening.m4r */ = {isa = PBXFileReference; lastKnownFileType = file; path = Opening.m4r; sourceTree = ""; }; 34C3C78E2040A4F70000134C /* sonarping.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; name = sonarping.mp3; path = Signal/AudioFiles/sonarping.mp3; sourceTree = SOURCE_ROOT; }; 34C3C7902040B0DC0000134C /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioPlayer.h; sourceTree = ""; }; @@ -1726,6 +1728,7 @@ 344DC9AD226E483C004E7322 /* Stickers */ = { isa = PBXGroup; children = ( + 34C2EEB52270FF7B00BCA1D0 /* LinearHorizontalLayout.swift */, 344DC9AE226E483C004E7322 /* ManageStickersViewController.swift */, 34C2EEAF2270B8E100BCA1D0 /* StickerKeyboard.swift */, 34C2EEB12270CC8D00BCA1D0 /* StickerPackCollectionView.swift */, @@ -3484,6 +3487,7 @@ 346129F51FD5F31400532771 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */, 45194F8F1FD71FF500333B2C /* ThreadUtil.m in Sources */, 34BEDB0E21C405B0007B0EAE /* ImageEditorModel.swift in Sources */, + 34C2EEB62270FF7C00BCA1D0 /* LinearHorizontalLayout.swift in Sources */, 451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */, 340872C122394CAA00CB25B0 /* ImageEditorTransform.swift in Sources */, 450C800F20AD1AB900F3A091 /* OWSWindowManager.m in Sources */, diff --git a/Signal/Images.xcassets/plus-24.imageset/Contents.json b/Signal/Images.xcassets/plus-24.imageset/Contents.json new file mode 100644 index 0000000000..72e7fa1e25 --- /dev/null +++ b/Signal/Images.xcassets/plus-24.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "plus-24@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "plus-24@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "plus-24@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/plus-24.imageset/plus-24@1x.png b/Signal/Images.xcassets/plus-24.imageset/plus-24@1x.png new file mode 100644 index 0000000000..5a11ea9a1d Binary files /dev/null and b/Signal/Images.xcassets/plus-24.imageset/plus-24@1x.png differ diff --git a/Signal/Images.xcassets/plus-24.imageset/plus-24@2x.png b/Signal/Images.xcassets/plus-24.imageset/plus-24@2x.png new file mode 100644 index 0000000000..1421a562e9 Binary files /dev/null and b/Signal/Images.xcassets/plus-24.imageset/plus-24@2x.png differ diff --git a/Signal/Images.xcassets/plus-24.imageset/plus-24@3x.png b/Signal/Images.xcassets/plus-24.imageset/plus-24@3x.png new file mode 100644 index 0000000000..d63c48c714 Binary files /dev/null and b/Signal/Images.xcassets/plus-24.imageset/plus-24@3x.png differ diff --git a/Signal/Images.xcassets/search-24.imageset/Contents.json b/Signal/Images.xcassets/search-24.imageset/Contents.json new file mode 100644 index 0000000000..56e31a38a0 --- /dev/null +++ b/Signal/Images.xcassets/search-24.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "search-24@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search-24@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "search-24@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/search-24.imageset/search-24@1x.png b/Signal/Images.xcassets/search-24.imageset/search-24@1x.png new file mode 100644 index 0000000000..f5692befb7 Binary files /dev/null and b/Signal/Images.xcassets/search-24.imageset/search-24@1x.png differ diff --git a/Signal/Images.xcassets/search-24.imageset/search-24@2x.png b/Signal/Images.xcassets/search-24.imageset/search-24@2x.png new file mode 100644 index 0000000000..5520b610fb Binary files /dev/null and b/Signal/Images.xcassets/search-24.imageset/search-24@2x.png differ diff --git a/Signal/Images.xcassets/search-24.imageset/search-24@3x.png b/Signal/Images.xcassets/search-24.imageset/search-24@3x.png new file mode 100644 index 0000000000..ddcb373baf Binary files /dev/null and b/Signal/Images.xcassets/search-24.imageset/search-24@3x.png differ diff --git a/SignalMessaging/ViewControllers/Stickers/LinearHorizontalLayout.swift b/SignalMessaging/ViewControllers/Stickers/LinearHorizontalLayout.swift new file mode 100644 index 0000000000..9b2522d043 --- /dev/null +++ b/SignalMessaging/ViewControllers/Stickers/LinearHorizontalLayout.swift @@ -0,0 +1,104 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import UIKit + +// A trivial layout that places each item in a horizontal line. +// Each item has uniform size. +class LinearHorizontalLayout: UICollectionViewLayout { + + private let itemSize: CGSize + private let inset: CGFloat + private let spacing: CGFloat + + private var itemAttributesMap = [UInt: UICollectionViewLayoutAttributes]() + + private var contentSize = CGSize.zero + + // MARK: Initializers and Factory Methods + + @available(*, unavailable, message:"use other constructor instead.") + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + required init(itemSize: CGSize, inset: CGFloat = 0, spacing: CGFloat = 0) { + self.itemSize = itemSize + self.inset = inset + self.spacing = spacing + + super.init() + } + + // MARK: Methods + + override func invalidateLayout() { + super.invalidateLayout() + + itemAttributesMap.removeAll() + } + + override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { + super.invalidateLayout(with: context) + + itemAttributesMap.removeAll() + } + + override func prepare() { + super.prepare() + + guard let collectionView = collectionView else { + return + } + guard collectionView.numberOfSections == 1 else { + owsFailDebug("This layout only support a single section.") + return + } + let itemCount = collectionView.numberOfItems(inSection: 0) + + let vInset: CGFloat = inset + let hInset: CGFloat = inset + + guard itemCount > 0 else { + contentSize = .zero + return + } + + for row in 0.. [UICollectionViewLayoutAttributes]? { + return itemAttributesMap.values.filter { itemAttributes in + return itemAttributes.frame.intersects(rect) + } + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let result = itemAttributesMap[UInt(indexPath.row)] + return result + } + + override var collectionViewContentSize: CGSize { + return contentSize + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + guard let collectionView = collectionView else { + return false + } + return collectionView.width() != newBounds.size.width + } +} diff --git a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift index 04ab2b297d..ae4f3ca9eb 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerKeyboard.swift @@ -43,8 +43,12 @@ public class StickerKeyboard: UIStackView { object: nil) } + required public init(coder: NSCoder) { + notImplemented() + } + // TODO: Tune this value. - private let keyboardHeight: CGFloat = 200 + private let keyboardHeight: CGFloat = 300 @objc public override var intrinsicContentSize: CGSize { @@ -59,31 +63,81 @@ public class StickerKeyboard: UIStackView { addBackgroundView(withBackgroundColor: Theme.offBackgroundColor) - headerView.axis = .horizontal addArrangedSubview(headerView) headerView.setContentHuggingVerticalHigh() headerView.setCompressionResistanceVerticalHigh() - headerView.autoSetDimension(.height, toSize: 44) stickerCollectionView.stickerDelegate = self addArrangedSubview(stickerCollectionView) stickerCollectionView.setContentHuggingVerticalLow() stickerCollectionView.setCompressionResistanceVerticalLow() + + populateHeaderView() } private func reloadStickers() { stickerPacks = StickerManager.installedStickerPacks() + packsCollectionView.collectionViewLayout.invalidateLayout() + packsCollectionView.reloadData() + guard stickerPacks.count > 0 else { - stickerPack = nil + stickerPack = nil return } if stickerPack == nil { stickerPack = stickerPacks.first } + } - // TODO: Reload header? + private let packsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: buildCoverLayout()) + private let cellReuseIdentifier = "cellReuseIdentifier" + + private static let packCoverSize: CGFloat = 24 + private static let packCoverSpacing: CGFloat = 12 + + private class func buildCoverLayout() -> UICollectionViewLayout { + return LinearHorizontalLayout(itemSize: CGSize(width: packCoverSize, height: packCoverSize), inset: 0, spacing: packCoverSpacing) + } + + private func populateHeaderView() { + headerView.spacing = StickerKeyboard.packCoverSpacing + headerView.axis = .horizontal + headerView.alignment = .center + headerView.backgroundColor = Theme.offBackgroundColor + headerView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) + headerView.isLayoutMarginsRelativeArrangement = true + + let searchButton = OWSButton(imageName: "search-24", tintColor: Theme.secondaryColor) { [weak self] in + self?.searchButtonWasTapped() + } + searchButton.setContentHuggingHigh() + searchButton.setCompressionResistanceHigh() + headerView.addArrangedSubview(searchButton) + + packsCollectionView.backgroundColor = Theme.offBackgroundColor + packsCollectionView.delegate = self + packsCollectionView.dataSource = self + packsCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier) + backgroundColor = Theme.offBackgroundColor + + packsCollectionView.setContentHuggingHorizontalLow() + packsCollectionView.setCompressionResistanceHorizontalLow() + packsCollectionView.autoSetDimension(.height, toSize: StickerKeyboard.packCoverSize) + headerView.addArrangedSubview(packsCollectionView) + + let manageButton = OWSButton(imageName: "plus-24", tintColor: Theme.secondaryColor) { [weak self] in + self?.manageButtonWasTapped() + } + manageButton.setContentHuggingHigh() + manageButton.setCompressionResistanceHigh() + headerView.addArrangedSubview(manageButton) + + updateHeaderView() + } + + private func updateHeaderView() { } // MARK: Events @@ -94,10 +148,23 @@ public class StickerKeyboard: UIStackView { Logger.verbose("") reloadStickers() + updateHeaderView() } - required public init(coder: NSCoder) { - notImplemented() + private func searchButtonWasTapped() { + AssertIsOnMainThread() + + Logger.verbose("") + + // TODO: + } + + private func manageButtonWasTapped() { + AssertIsOnMainThread() + + Logger.verbose("") + + // TODO: } } @@ -112,3 +179,51 @@ extension StickerKeyboard: StickerPackCollectionViewDelegate { delegate?.didSelectSticker(stickerInfo: stickerInfo) } } + +// MARK: - UICollectionViewDelegate + +extension StickerKeyboard: UICollectionViewDelegate { + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + Logger.debug("") + + guard let stickerPack = stickerPacks[safe: indexPath.row] else { + owsFailDebug("Invalid index path: \(indexPath)") + return + } + + self.stickerPack = stickerPack + } +} + +// MARK: - UICollectionViewDataSource + +extension StickerKeyboard: UICollectionViewDataSource { + + public func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection sectionIdx: Int) -> Int { + return stickerPacks.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + // We could eventually use cells that lazy-load the sticker views + // when the cells becomes visible and eagerly unload them. + // But we probably won't need to do that. + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseIdentifier, for: indexPath) + + guard let stickerPack = stickerPacks[safe: indexPath.row] else { + owsFailDebug("Invalid index path: \(indexPath)") + return cell + } + + // TODO: Actual size? + let iconView = StickerView(stickerInfo: stickerPack.coverInfo) + + cell.contentView.addSubview(iconView) + iconView.autoPinEdgesToSuperviewEdges() + + return cell + } +} diff --git a/SignalMessaging/ViewControllers/Stickers/StickerPackCollectionView.swift b/SignalMessaging/ViewControllers/Stickers/StickerPackCollectionView.swift index 2ab9174a9e..c65505b670 100644 --- a/SignalMessaging/ViewControllers/Stickers/StickerPackCollectionView.swift +++ b/SignalMessaging/ViewControllers/Stickers/StickerPackCollectionView.swift @@ -22,6 +22,8 @@ public class StickerPackCollectionView: UICollectionView { AssertIsOnMainThread() reloadStickers() + // Scroll to the top. + contentOffset = .zero } } @@ -151,6 +153,8 @@ extension StickerPackCollectionView { } layout.minimumInteritemSpacing = kSpacing layout.minimumLineSpacing = kSpacing + let inset = kSpacing + layout.sectionInset = UIEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset) return layout } @@ -170,9 +174,11 @@ extension StickerPackCollectionView { } let spacing = StickerPackCollectionView.kSpacing - let preferredCellSize: CGFloat = 84 - let columnCount = UInt((containerWidth + spacing) / (preferredCellSize + spacing)) - let cellWidth = (containerWidth - spacing * (CGFloat(columnCount) - 1)) / CGFloat(columnCount) + let inset = spacing + let preferredCellSize: CGFloat = 80 + let contentWidth = containerWidth - 2 * inset + let columnCount = UInt((contentWidth + spacing) / (preferredCellSize + spacing)) + let cellWidth = (contentWidth - spacing * (CGFloat(columnCount) - 1)) / CGFloat(columnCount) let itemSize = CGSize(width: cellWidth, height: cellWidth) if (itemSize != flowLayout.itemSize) {